<|start_filename|>src/Ulid.cs<|end_filename|>
﻿#nullable enable
using System;
using System.Buffers.Binary;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

namespace Lidgren.Core
{
	/// <summary>
	/// Alternative to Guid; see https://github.com/ulid/spec
	/// </summary>
	[StructLayout(LayoutKind.Sequential)]
	public readonly struct Ulid : IEquatable<Ulid>, IComparable<Ulid>
	{
		public const int SizeInBytes = 16;

		public static readonly Ulid Empty = new Ulid(0, 0);

		private static readonly long s_timeOffset = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - (long)TimeService.TicksToMilliSeconds(Stopwatch.GetTimestamp());

		public readonly ulong Low;
		public readonly ulong High;

		private const string k_encode = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
		private static readonly byte[] s_decode = new byte[] { 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 255, 255, 255, 255, 255, 255, 255, 10, 11, 12, 13, 14, 15, 16, 17, 255, 18, 19, 255, 20, 21, 255, 22, 23, 24, 25, 26, 255, 27, 28, 29, 30, 31, 255, 255, 255, 255, 255, 255, 10, 11, 12, 13, 14, 15, 16, 17, 255, 18, 19, 255, 20, 21, 255, 22, 23, 24, 25, 26, 255, 27, 28, 29, 30, 31 };

		public Ulid(ulong low, ulong high)
		{
			Low = low;
			High = high;
		}

		public Ulid(ReadOnlySpan<byte> fromBytes)
		{
			CoreException.Assert(fromBytes.Length >= 16);
			High = BinaryPrimitives.ReadUInt64LittleEndian(fromBytes.Slice(0, 8));
			Low = BinaryPrimitives.ReadUInt64LittleEndian(fromBytes.Slice(8, 8));
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static Ulid Create()
		{
			var time = s_timeOffset + TimeService.TicksToMilliSeconds(Stopwatch.GetTimestamp());
			return new Ulid(PRNG.NextUInt64(), ((ulong)time << 16) | (PRNG.NextUInt64() & 0xFFFF));
		}

		public static Ulid Create(ref ulong rndState)
		{
			var time = s_timeOffset + TimeService.TicksToMilliSeconds(Stopwatch.GetTimestamp());
			return new Ulid(PRNG.NextUInt64(ref rndState), ((ulong)time << 16) | (PRNG.NextUInt64(ref rndState) & 0xFFFF));
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static Ulid Create(in DateTimeOffset dto)
		{
			var time = dto.ToUnixTimeMilliseconds();
			return new Ulid(PRNG.NextUInt64(), ((ulong)time << 16) | (PRNG.NextUInt64() & 0xFFFF));
		}

		public static Ulid Create(in DateTimeOffset dto, ref ulong rndState)
		{
			var time = dto.ToUnixTimeMilliseconds();
			return new Ulid(PRNG.NextUInt64(ref rndState), ((ulong)time << 16) | (PRNG.NextUInt64(ref rndState) & 0xFFFF));
		}

		public static Ulid Parse(ReadOnlySpan<char> fromString)
		{
			CoreException.Assert(fromString.Length >= 26);
#if DEBUG
			for (int i = 0; i < 26; i++)
			{
				var c = fromString[i];
				if (s_decode[c] == 255)
					throw new Exception("Bad char in Ulid string: " + c);
			}
#endif
			ulong high =
				(ulong)s_decode[fromString[0]] << 61 |
				((ulong)s_decode[fromString[1]] << 56) |
				((ulong)s_decode[fromString[2]] << 51) |
				((ulong)s_decode[fromString[3]] << 46) |
				((ulong)s_decode[fromString[4]] << 41) |
				((ulong)s_decode[fromString[5]] << 36) |
				((ulong)s_decode[fromString[6]] << 31) |
				((ulong)s_decode[fromString[7]] << 26) |
				((ulong)s_decode[fromString[8]] << 21) |
				((ulong)s_decode[fromString[9]] << 16) |
				((ulong)s_decode[fromString[10]] << 11) |
				((ulong)s_decode[fromString[11]] << 6) |
				((ulong)s_decode[fromString[12]] << 1);

			ulong straddle = s_decode[fromString[13]];
			high |= (straddle >> 4); // 1 bit from straddle to high

			ulong low = (ulong)straddle << 60 | // keep 4 bits from straddle
				((ulong)s_decode[fromString[14]] << 55) |
				((ulong)s_decode[fromString[15]] << 50) |
				((ulong)s_decode[fromString[16]] << 45) |
				((ulong)s_decode[fromString[17]] << 40) |
				((ulong)s_decode[fromString[18]] << 35) |
				((ulong)s_decode[fromString[19]] << 30) |
				((ulong)s_decode[fromString[20]] << 25) |
				((ulong)s_decode[fromString[21]] << 20) |
				((ulong)s_decode[fromString[22]] << 15) |
				((ulong)s_decode[fromString[23]] << 10) |
				((ulong)s_decode[fromString[24]] << 5) |
				((ulong)s_decode[fromString[25]]);

			return new Ulid(low, high);
		}

		public void AsString(Span<char> into)
		{
			into[0] = k_encode[(int)(High >> 61)];
			into[1] = k_encode[(int)((High >> 56) & 0b11111)];
			into[2] = k_encode[(int)((High >> 51) & 0b11111)];
			into[3] = k_encode[(int)((High >> 46) & 0b11111)];
			into[4] = k_encode[(int)((High >> 41) & 0b11111)];
			into[5] = k_encode[(int)((High >> 36) & 0b11111)];
			into[6] = k_encode[(int)((High >> 31) & 0b11111)];
			into[7] = k_encode[(int)((High >> 26) & 0b11111)];
			into[8] = k_encode[(int)((High >> 21) & 0b11111)];
			into[9] = k_encode[(int)((High >> 16) & 0b11111)];
			into[10] = k_encode[(int)((High >> 11) & 0b11111)];
			into[11] = k_encode[(int)((High >> 6) & 0b11111)];
			into[12] = k_encode[(int)((High >> 1) & 0b11111)];

			var straddle = ((High & 0b1) << 4) | ((Low >> 60) & 0b1111);
			into[13] = k_encode[(int)straddle];

			into[14] = k_encode[(int)((Low >> 55) & 0b11111)];
			into[15] = k_encode[(int)((Low >> 50) & 0b11111)];
			into[16] = k_encode[(int)((Low >> 45) & 0b11111)];
			into[17] = k_encode[(int)((Low >> 40) & 0b11111)];
			into[18] = k_encode[(int)((Low >> 35) & 0b11111)];
			into[19] = k_encode[(int)((Low >> 30) & 0b11111)];
			into[20] = k_encode[(int)((Low >> 25) & 0b11111)];
			into[21] = k_encode[(int)((Low >> 20) & 0b11111)];
			into[22] = k_encode[(int)((Low >> 15) & 0b11111)];
			into[23] = k_encode[(int)((Low >> 10) & 0b11111)];
			into[24] = k_encode[(int)((Low >> 5) & 0b11111)];
			into[25] = k_encode[(int)(Low & 0b11111)];
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void AsBytes(Span<byte> into)
		{
			into[0] = (byte)High;
			into[1] = (byte)(High >> 8);
			into[2] = (byte)(High >> 16);
			into[3] = (byte)(High >> 24);
			into[4] = (byte)(High >> 32);
			into[5] = (byte)(High >> 40);
			into[6] = (byte)(High >> 48);
			into[7] = (byte)(High >> 56);
			into[8] = (byte)Low;
			into[9] = (byte)(Low >> 8);
			into[10] = (byte)(Low >> 16);
			into[11] = (byte)(Low >> 24);
			into[12] = (byte)(Low >> 32);
			into[13] = (byte)(Low >> 40);
			into[14] = (byte)(Low >> 48);
			into[15] = (byte)(Low >> 56);
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void GetTime(out DateTimeOffset time)
		{
			time = DateTimeOffset.FromUnixTimeMilliseconds((long)(High >> 16));
		}

		public override string ToString()
		{
			return String.Create<Ulid>(26, this, (span, ulid) =>
			{
				ulid.AsString(span);
			});
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public override bool Equals(object? other)
		{
			if (other is null || !(other is Ulid))
				return false;
			var oulid = (Ulid)other;
			return oulid.Low == Low && oulid.High == High;
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public bool Equals(Ulid other)
		{
			return other.Low == Low && other.High == High;
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public bool Equals(in Ulid other)
		{
			return other.Low == Low && other.High == High;
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static bool operator ==(in Ulid x, in Ulid y)
		{
			return (x.Low == y.Low && x.High == y.High);
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static bool operator !=(in Ulid x, in Ulid y)
		{
			return (x.Low != y.Low || x.High != y.High);
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public override int GetHashCode()
		{
			// Low is all random bits; can be used verbatim
			// But the time part of High needs to be mixed a bit
			unchecked
			{
				// simple mixing since we're xoring with random bits anyway
				ulong mixed = High;
				mixed ^= mixed >> 47;
				mixed *= 0xc6a4a7935bd1e995ul;
				mixed ^= mixed >> 47;

				ulong hash = mixed ^ Low;

				// xor fold to 32 bits
				return (int)((uint)hash ^ (uint)(hash >> 32));
			}
		}

		public int CompareTo(Ulid other)
		{
			if (High < other.High)
				return -1;
			if (High > other.High)
				return 1;
			if (Low < other.Low)
				return -1;
			if (Low > other.Low)
				return 1;
			return 0;
		}
	}
}


<|start_filename|>src/Collections/BitVector.cs<|end_filename|>
﻿using System;
using System.Diagnostics;
using System.Numerics;
using System.Runtime;
using System.Runtime.CompilerServices;

namespace Lidgren.Core
{
	/// <summary>
	/// Fixed arbitrarily long bit vector; range verification only done in DEBUG
	/// </summary>
	public sealed class BitVector
	{
		private const int c_bitsPerUnit = 64;
		private const int c_bitsMask = 63;
		private const int c_unitBitCount = 6;

		private ulong[] m_data;
		public ulong[] Data => m_data;

#if DEBUG
		private int m_exactSize;
#endif

		/// <summary>
		/// Number of bits in the vector
		/// </summary>
		public int Size => m_data.Length * 64;

		public BitVector(int minSize)
		{
			int numUnits = UnitsToHoldBits(minSize);
			m_data = new ulong[numUnits];
#if DEBUG
			m_exactSize = minSize;
#endif
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		private int UnitsToHoldBits(int bits)
		{
			return ((bits - 1) / c_bitsPerUnit) + 1;
		}

		public bool this[int index]
		{
			get { return Get(index); }
			set { Set(index, value); }
		}

		/// <summary>
		/// Clear all values to false
		/// </summary>
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void Clear()
		{
			m_data.AsSpan().Clear();
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void SetAll(bool value)
		{
			if (value)
				m_data.AsSpan().Fill(~0ul);
			else
				m_data.AsSpan().Clear();
		}

		[Conditional("DEBUG")]
		private void VerifyIndex(int index)
		{
#if DEBUG
			if (index < 0 || index >= m_exactSize)
				CoreException.Throw("Index out of range");
#endif
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public bool Get(int index)
		{
			VerifyIndex(index);
			int dataIndex = index >> c_unitBitCount;
			ulong value = m_data[dataIndex];
			int bitIndex = index & c_bitsMask;
			return ((value >> bitIndex) & 1ul) != 0ul;
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void Flip(int index)
		{
			VerifyIndex(index);
			int dataIndex = index >> c_unitBitCount;
			int bitIndex = index & c_bitsMask;
			ulong mask = 1ul << bitIndex;
			m_data[dataIndex] ^= mask;
		}

		/// <summary>
		/// Set index to true
		/// </summary>
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void Set(int index)
		{
			VerifyIndex(index);
			int dataIndex = index >> c_unitBitCount;
			int bitIndex = index & c_bitsMask;
			ulong mask = 1ul << bitIndex;
			m_data[dataIndex] |= mask;
		}

		/// <summary>
		/// Set index to false
		/// </summary>
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void Clear(int index)
		{
			VerifyIndex(index);
			int dataIndex = index >> c_unitBitCount;
			int bitIndex = index & c_bitsMask;
			ulong mask = 1ul << bitIndex;
			m_data[dataIndex] &= ~mask;
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void Set(int index, bool value)
		{
			VerifyIndex(index);
			int dataIndex = index >> c_unitBitCount;
			int bitIndex = index & c_bitsMask;
			ulong mask = 1ul << bitIndex;
			if (value)
				m_data[dataIndex] |= mask;
			else
				m_data[dataIndex] &= ~mask;
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public int CountSetBits()
		{
			int retval = 0;
			for (int i = 0; i < m_data.Length; i++)
				retval += BitOperations.PopCount(m_data[i]);
			return retval;
		}
	}
}


<|start_filename|>src/JobService/JobService.ForEachArg.cs<|end_filename|>
﻿using System;

namespace Lidgren.Core
{
	public static partial class JobService
	{
		/// <summary>
		/// Enqueue work to be called once per argument in list; calls continuation(continuationArgument) when all completed
		/// </summary>
		public static void ForEachArgument(string name, Action<object> work, ReadOnlySpan<object> arguments, Action<object> continuation, object continuationArgument)
		{
			CoreException.Assert(s_workers != null, "JobService not initialized");

			int numJobs = arguments.Length;

			if (numJobs == 0)
				return;

			var completion = JobCompletion.Acquire();
			completion.ContinuationAtCount = numJobs;
			completion.Continuation = continuation;
			completion.ContinuationArgument = continuationArgument;
#if DEBUG
			completion.ContinuationName = name + "Contd";
#endif
			lock (s_instances)
			{
				foreach (var argument in arguments)
					EnqueueInternal(name, work, argument, completion);
			}
		}

		/// <summary>
		/// Enqueue work to be called once per argument in list; blocks until all completed
		/// </summary>
		public static void ForEachArgumentBlock<TArg>(string name, Action<object> work, ReadOnlySpan<TArg> arguments)
		{
			CoreException.Assert(s_workers != null, "JobService not initialized");

			if (arguments.Length == 0)
				return;

			if (arguments.Length == 1)
			{
				work(arguments[0]);
				return;
			}

			int numJobs = arguments.Length - 1;
			var completion = JobCompletion.Acquire();
			lock (s_instances)
			{
				foreach (var argument in arguments.Slice(1))
					EnqueueInternal(name, work, argument, completion);
			}

			// run one time on this thread
			using (new Timing(name))
				work(arguments[0]);

			// then, try to steal relevant jobs if possible

			// am I running within a job? try fetching the JobWorker for this thread
			var localWorker = JobWorker.WorkerForThread;
			while (JobService.ExecuteAnyJob(localWorker, completion))
			;

			completion.WaitAndRelease(numJobs);
		}
	}
}


<|start_filename|>src/DataWriter.cs<|end_filename|>
﻿#nullable enable
using System;
using System.Buffers;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

namespace Lidgren.Core
{
	/// <summary>
	/// Expanding buffer using ArrayPool{byte} backing; get results using ReadOnlySpan and remember to dispose
	/// </summary>
	public struct DataWriter : IDisposable
	{
		private byte[] m_buffer;
		private int m_length;

		public DataWriter(int initialCapacity)
		{
			m_buffer = ArrayPool<byte>.Shared.Rent(initialCapacity);
			m_length = 0;
		}

		public void Dispose()
		{
			if (m_buffer != null)
				ArrayPool<byte>.Shared.Return(m_buffer);
		}

		public Span<byte> Span => m_buffer.AsSpan(0, m_length);
		public ReadOnlySpan<byte> ReadOnlySpan => m_buffer.AsSpan(0, m_length);

		public void Clear()
		{
			m_length = 0;
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public Span<byte> Allocate(int numBytes)
		{
			int len = m_length;
			int remaining = m_buffer.Length - len;
			if (numBytes > remaining)
				Grow(len + numBytes);
			m_length = len + numBytes;
			return m_buffer.AsSpan(len, numBytes);
		}

		/// <summary>
		/// Grow capacity to be able to add this many bytes without re-allocating
		/// </summary>
		public void EnsureCapacity(int size)
		{
			int minTotalSize = m_length + size;
			if (m_buffer.Length > minTotalSize)
				return; // we're good
			Grow(minTotalSize);
		}

		[MethodImpl(MethodImplOptions.NoInlining)]
		private void Grow(int minSize)
		{
			// grow more carefully than doubling; esp. since ArrayPool may return longer
			int curLen = m_buffer.Length;
			int grow = 16 + (curLen / 4);
			int newSize = Math.Max(curLen + grow, minSize);

			var newBuf = ArrayPool<byte>.Shared.Rent(newSize);
			ReadOnlySpan.CopyTo(newBuf);

			ArrayPool<byte>.Shared.Return(m_buffer);
			m_buffer = newBuf;
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void WriteBool(bool value)
		{
			Allocate(1)[0] = value ? (byte)1 : (byte)0;
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void WriteUInt16(ushort value)
		{
			var into = Allocate(2);
			into.WriteUInt16(value);
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void WriteUInt32(uint value)
		{
			var into = Allocate(4);
			into.WriteUInt32(value);
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void WriteInt32(int value)
		{
			var into = Allocate(4);
			into.WriteInt32(value);
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void WriteUInt64(ulong value)
		{
			var into = Allocate(8);
			into.WriteUInt64(value);
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void WriteVariableUInt64(ulong value)
		{
			var into = Allocate(9); // worst case
			into.WriteVariableUInt64(value);
			m_length -= into.Length; // return unused space
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void WriteVariableInt64(long value)
		{
			var into = Allocate(9); // worst case
			into.WriteVariableInt64(value);
			m_length -= into.Length; // return unused space
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void WriteVariableUInt32(uint value)
		{
			var into = Allocate(5); // worst case
			into.WriteVariableUInt64(value);
			m_length -= into.Length; // return unused space
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void WriteVariableInt32(int value)
		{
			var into = Allocate(5); // worst case
			into.WriteVariableInt64(value);
			m_length -= into.Length; // return unused space
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void WriteSingle(float value)
		{
			var into = Allocate(4);
			into.WriteSingle(value);
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void WriteFloat(float value)
		{
			var into = Allocate(4);
			into.WriteSingle(value);
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void WriteDouble(double value)
		{
			var into = Allocate(8);
			into.WriteDouble(value);
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void WriteBytes(ReadOnlySpan<byte> data)
		{
			var into = Allocate(data.Length);
			data.CopyTo(into);
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void WriteByte(byte value)
		{
			Allocate(1)[0] = value;
		}

		public void WriteString(ReadOnlySpan<char> str)
		{
			int curLen = m_length;
			var minCap = curLen + 2 + str.Length;

			// grow?
			if (minCap > m_buffer.Length)
				Grow(minCap);

			for (; ; )
			{
				var span = m_buffer.AsSpan(curLen);
				bool ok = span.WriteString(str);
				if (ok)
				{
					m_length = m_buffer.Length - span.Length;
					return;
				}
				Grow(curLen + 2 + (str.Length * 2));
			}
		}

		/// <summary>
		/// Write length prefixed array of T
		/// </summary>
		public void WriteLengthPrefixedArray<T>(ReadOnlySpan<T> span) where T : unmanaged
		{
			WriteVariableUInt32((uint)span.Length);
			if (span.Length == 0)
				return;
			var src = MemoryMarshal.AsBytes(span);
			var into = Allocate(src.Length);
			src.CopyTo(into);
		}

		/// <summary>
		/// Write an unmanaged struct (ie containing no reference types)
		/// </summary>
		public void Write<T>(T value) where T : unmanaged
		{
			var into = Allocate(Unsafe.SizeOf<T>());
			MemoryMarshal.Write<T>(into, ref value);
		}

		public void WriteVector2(in Vector2 value)
		{
			var into = Allocate(8);
			var xslice = into.Slice(0, 4);
			xslice.WriteSingle(value.X);
			var yslice = into.Slice(4, 4);
			yslice.WriteSingle(value.Y);
		}

		public void WriteVector3(in Vector3 value)
		{
			var into = Allocate(12);
			var xslice = into.Slice(0, 4);
			xslice.WriteSingle(value.X);
			var yslice = into.Slice(4, 4);
			yslice.WriteSingle(value.Y);
			var zslice = into.Slice(8, 4);
			zslice.WriteSingle(value.Z);
		}

		public void WriteVector4(in Vector4 value)
		{
			var into = Allocate(16);
			var xslice = into.Slice(0, 4);
			xslice.WriteSingle(value.X);
			var yslice = into.Slice(4, 4);
			yslice.WriteSingle(value.Y);
			var zslice = into.Slice(8, 4);
			zslice.WriteSingle(value.Z);
			var wslice = into.Slice(12, 4);
			wslice.WriteSingle(value.W);
		}
	}
}


<|start_filename|>tests/PriorityQueueTests.cs<|end_filename|>
﻿using System;
using System.Collections.Generic;
using Lidgren.Core;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace UnitTests
{
	[TestClass]
	public partial class PriorityQueueTests
	{
		[TestMethod]
		public void TestPriorityQueue()
		{
			{
				// just enqueue a bunch
				var pq = new PriorityQueue<uint, uint>(100);
				for (int i = 0; i < 1000; i++)
				{
					var r = PRNG.NextUInt32();
					if (r == 0)
						r = 1;
					pq.Enqueue(r, r);
				}

				// dequeue and verify
				uint last = 0;
				while (pq.Count > 0)
				{
					var ok = pq.Peek(out var nextprio, out var nextItem);
					Assert.IsTrue(ok);
					Assert.AreEqual(nextprio, nextItem);
					Assert.IsTrue(nextprio >= last);
					last = nextprio;

					var pok = pq.TryDequeue(out var dequeuedItem);
					Assert.IsTrue(pok);
					Assert.AreEqual(dequeuedItem, nextItem);
				}
			}

			{
				var queue = new PriorityQueue<int, string>(4);

				queue.Enqueue(5, "five");
				queue.Enqueue(3, "three");
				queue.Enqueue(8, "eight");
				queue.Enqueue(1, "one");
				queue.Enqueue(3, "three-again");

				bool ok = queue.TryDequeue(out var str);
				Assert.IsTrue(ok);
				Assert.AreEqual("one", str);

				ok = queue.TryDequeue(out str);
				Assert.IsTrue(ok);
				Assert.IsTrue(str == "three" || str == "three-again");
				ok = queue.TryDequeue(out str);
				Assert.IsTrue(ok);
				Assert.IsTrue(str == "three" || str == "three-again");

				ok = queue.PeekPriority(out var nextPrio);
				Assert.IsTrue(ok);
				Assert.AreEqual(5, nextPrio);

				ok = queue.TryDequeue(out str);
				Assert.IsTrue(ok);
				Assert.AreEqual("five", str);

				Assert.AreEqual(1, queue.Count);

				queue.Enqueue(6, "six");

				ok = queue.TryDequeue(out str);
				Assert.IsTrue(ok);
				Assert.AreEqual("six", str);

				ok = queue.TryDequeue(out str);
				Assert.IsTrue(ok);
				Assert.AreEqual("eight", str);

				ok = queue.TryDequeue(out str);
				Assert.IsFalse(ok);
				Assert.AreEqual(0, queue.Count);

				queue.Clear();
				queue.Enqueue(3, "3");
				queue.Enqueue(1, "1");
				queue.Enqueue(2, "2");
				ok = queue.Remove("22");
				Assert.IsFalse(ok);
				Assert.AreEqual(3, queue.Count);
				ok = queue.Remove("2");
				Assert.IsTrue(ok);

				Assert.AreEqual("1", queue.Dequeue());
				Assert.AreEqual("3", queue.Dequeue());
				ok = queue.TryDequeue(out _);
				Assert.IsFalse(ok);

				var facit = new List<(int, string)>();
				queue.Clear();
				for (int op = 0; op < 1000000; op++)
				{
					switch (PRNG.Next(0, 8))
					{
						case 0:
							queue.Clear();
							facit.Clear();
							break;
						case 1:
						case 2:
						case 3:
						case 4:
							// enqueue
							int add = PRNG.Next(0, 50);
							queue.Enqueue(add, add.ToString());
							facit.Add((add, add.ToString()));
							break;
						case 5:
							// remove
							int rem = PRNG.Next(0, 50);
							var fok = Remove(facit, rem.ToString());
							var qok = queue.Remove(rem.ToString());
							Assert.AreEqual(fok, qok);
							break;
						case 6:
						case 7:
							// dequeue
							if (facit.Count <= 0)
								continue;
							var fitem = Dequeue(facit);
							var qitem = queue.Dequeue();
							Assert.AreEqual(fitem, qitem);
							break;
					}

					Assert.AreEqual(facit.Count, queue.Count);
					if (facit.Count > 0)
					{
						ok = queue.Peek(out var peekprio, out var peekitem);
						Assert.IsTrue(ok);
						Peek(facit, out var facitpeekprio, out var facitpeekitem);

						Assert.AreEqual(facitpeekprio, peekprio);
						Assert.AreEqual(facitpeekitem, peekitem);
					}
				}
			}
		}

		private void Peek(List<(int, string)> facit, out int facitpeekprio, out string facitpeekitem)
		{
			int lowestIndex = -1;
			int lowestPrio = int.MaxValue;
			for (int i = 0; i < facit.Count; i++)
			{
				var prio = facit[i].Item1;
				if (prio < lowestPrio)
				{
					lowestPrio = prio;
					lowestIndex = i;
				}
			}

			Assert.IsTrue(lowestIndex != -1);

			facitpeekprio = facit[lowestIndex].Item1;
			facitpeekitem = facit[lowestIndex].Item2;
		}

		private string Dequeue(List<(int, string)> facit)
		{
			int lowestIndex = -1;
			int lowestPrio = int.MaxValue;
			for (int i = 0; i < facit.Count; i++)
			{
				if (facit[i].Item1 < lowestPrio)
				{
					lowestPrio = facit[i].Item1;
					lowestIndex = i;
				}
			}

			var retval = facit[lowestIndex].Item2;
			facit.RemoveAt(lowestIndex);
			return retval;
		}

		private bool Remove(List<(int, string)> facit, string item)
		{
			for (int i = 0; i < facit.Count; i++)
			{
				if (facit[i].Item2 == item)
				{
					facit.RemoveAt(i);
					return true;
				}
			}
			return false;
		}
	}
}


<|start_filename|>src/Collections/FastList/FastList.Foreach.cs<|end_filename|>
﻿using System;
using System.Runtime.CompilerServices;

namespace Lidgren.Core
{
	public partial class FastList<T>
	{
		//
		// foreach support; however, iterating over myList.ReadOnlySpan instead is recommended
		//

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public FastListIterator GetEnumerator()
		{
			FastListIterator it;
			it.Span = new Span<T>(m_buffer, m_offset, m_count);
			it.Index = 0;
			return it;
		}

		public ref struct FastListIterator
		{
			public ReadOnlySpan<T> Span;
			public int Index;

			public ref readonly T Current
			{
				[MethodImpl(MethodImplOptions.AggressiveInlining)]
				get
				{
					return ref Span[Index - 1];
				}
			}

			[MethodImpl(MethodImplOptions.AggressiveInlining)]
			public bool MoveNext()
			{
				Index++;
				return Index <= Span.Length;
			}
		}
	}
}


<|start_filename|>src/File/FileHash.cs<|end_filename|>
﻿#nullable enable
using System;
using System.Buffers;
using System.IO;

namespace Lidgren.Core
{
	/// <summary>
	/// Hash files using Hasher
	/// </summary>
	public static class FileHash
	{
		public static ulong Hash(string fileName)
		{
			using (var fs = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete))
				return Hash(fs, out _);
		}

		public static ulong Hash(ReadOnlySpan<byte> bytes)
		{
			var hasher = Hasher.Create();
			hasher.Add(bytes);
			return hasher.Finalize64();
		}

		public static ulong Hash(Stream stream, out int bytesHashedCount)
		{
			// read 4k at a time
			const int bufSize = 1024 * 4;
			var backing = ArrayPool<byte>.Shared.Rent(bufSize);
			try
			{
				var buffer = backing.AsSpan(0, bufSize);
				var hasher = Hasher.Create();
				bytesHashedCount = 0;
				for (; ; )
				{
					int numBytes = stream.Read(buffer);
					if (numBytes > 0)
					{
						bytesHashedCount += numBytes;
						hasher.Add(buffer.Slice(0, numBytes));
					}
					else
						break;
				}
				return hasher.Finalize64();
			}
			finally
			{
				ArrayPool<byte>.Shared.Return(backing);
			}
		}
	}
}


<|start_filename|>src/RandomSeed.cs<|end_filename|>
﻿using System;
using System.Diagnostics;
using System.Threading;
using System.Buffers.Binary;

namespace Lidgren.Core
{
	public static class RandomSeed
	{
		private static volatile int s_seedIncrement;
		private static readonly Random s_systemRandom;

		static RandomSeed()
		{
			s_seedIncrement = 997;

			int sysSeed = (int)HashUtil.XorFold64To32((ulong)Stopwatch.GetTimestamp() ^ (ulong)Environment.TickCount);
			s_systemRandom = new Random(sysSeed);
		}

		/// <summary>
		/// Generates a 64 bit random seed by combining various factors that are very hard to predict
		/// </summary>
		public static ulong GetUInt64()
		{
			unchecked
			{
				// start with bits from new guid
				Span<byte> span = stackalloc byte[16];
				Guid.NewGuid().TryWriteBytes(span);
				ulong guidLow = BinaryPrimitives.ReadUInt64LittleEndian(span);
				ulong guidHigh = BinaryPrimitives.ReadUInt64LittleEndian(span.Slice(8));
				ulong v1 = guidLow ^ guidHigh;

				// xor bits from system random
				s_systemRandom.NextBytes(span);
				ulong rndLow = BinaryPrimitives.ReadUInt64LittleEndian(span);
				ulong rndHigh = BinaryPrimitives.ReadUInt64LittleEndian(span.Slice(8));
				ulong v2 = (rndLow ^ rndHigh);

				// add time as source of randomness as well
				v1 ^= (((ulong)Environment.TickCount << 30) ^ (ulong)Stopwatch.GetTimestamp());

				// ensure rapid invocations differ
				v2 ^= ((ulong)Interlocked.Add(ref s_seedIncrement, s_seedIncrement) * 7199369ul);

#if NET5_0_OR_GREATER
				// add processid in case multiple process is running concurrently
				v1 ^= (ulong)Environment.ProcessId << 16;
#endif

				// merge results
				ulong result = v1 ^ v2;

				// make sure at least ONE bit is set to ensure the seed is never zero
				// which is a very problematic numbers for some PRNGs
				if (result == 0)
					return GetUInt64();

				return result;
			}
		}

		/// <summary>
		/// Generates a 32 bit random seed by combining various factors that are very hard to predict
		/// </summary>
		public static uint GetUInt32()
		{
			unchecked
			{
				var value = GetUInt64();
				return (uint)value ^ (uint)(value >> 32);
			}
		}

		/// <summary>
		/// Expand 32 bit seed to 64 bit deterministically, ensuring there are bits set in the upper part
		/// </summary>
		public static ulong ExpandSeed(uint seed)
		{
			unchecked
			{
				ulong upper = (ulong)HashUtil.XorFold64To32(seed * 7199369) << 32;
				return ((ulong)seed | upper) ^ 0b11100000_01111101_01000011_11110110_01001111_10100000_01100000_10011011UL;
			}
		}
	}
}


<|start_filename|>tests/FastDictionaryTests.cs<|end_filename|>
﻿using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;

using Lidgren.Core;
using System.Collections.Generic;

namespace UnitTests
{
	[TestClass]
	public class FastDictionaryTests
	{
		[TestMethod]
		public void TestFastDictionary()
		{
			var facit = new Dictionary<int, string>();
			var fast = new FastDictionary<int, string>();

			for (int r = 0; r < 1000000; r++)
			{
				var rnd = PRNG.Next(0, 13);
				switch (rnd)
				{
					case 0:
						facit.Clear();
						fast.Clear();
						break;
					case 1:
					case 2:
						{
							// add/change using indexer
							int key;
							if (facit.Count > 2 && PRNG.Next(0, 100) < 20)
								key = GetRandomKey(facit);
							else
								key = PRNG.Next(1, 1000);
							facit[key] = key.ToString();
							fast[key] = key.ToString();
							break;
						}
					case 3:
					case 4:
						// add using Add()
						int b = PRNG.Next(1, 1000);
						if (facit.ContainsKey(b) == false)
						{
							facit.Add(b, b.ToString());
							fast.Add(b, b.ToString());
						}
						break;
					case 5:
					case 6:
						{
							// add using GetOrInit()
							int c = PRNG.Next(0, 1000);
							bool exists = facit.ContainsKey(c);
							ref var str = ref fast.GetOrInit(c, out bool wasCreated);
							Assert.AreEqual(exists, !wasCreated);
							str = c.ToString();
							if (!exists)
								facit[c] = c.ToString();
							break;
						}
					case 7:
					case 8:
						{
							// remove existing
							if (facit.Count < 1)
								break;
							var key = GetRandomKey(facit);
							var ok = facit.Remove(key);
							Assert.IsTrue(ok);
							ok = fast.Remove(key);
							Assert.IsTrue(ok);
							break;
						}
					case 9:
					case 10:
					case 11:
						{
							// check existing
							if (facit.Count == 0)
								break;
							var key = GetRandomKey(facit);
							Assert.IsTrue(facit.ContainsKey(key));
							Assert.IsTrue(fast.ContainsKey(key));

							bool ok = fast.TryGetValue(key, out var val);
							Assert.IsTrue(ok);
							Assert.AreEqual(key.ToString(), val);

							ref var str = ref fast.TryGetRef(key, out bool wasFound);
							Assert.IsTrue(wasFound);
							Assert.AreEqual(key.ToString(), str);
						}
						break;
					case 12:
						{
							// check non-existing
							var key = int.MaxValue;
							Assert.IsFalse(facit.ContainsKey(key));
							Assert.IsFalse(fast.ContainsKey(key));

							bool ok = fast.TryGetValue(key, out var val);
							Assert.IsFalse(ok);

							ref var str = ref fast.TryGetRef(key, out bool wasFound);
							Assert.IsFalse(wasFound);
						}
						break;
				}

#if DEBUG
				fast.Validate();
#endif
				Assert.AreEqual(facit.Count, fast.Count);
				foreach (var key in facit.Keys)
					Assert.IsTrue(fast.ContainsKey(key));
				foreach (var value in facit.Values)
					Assert.IsTrue(fast.ContainsValue(value));
				foreach (var key in fast.Keys)
					Assert.IsTrue(facit.ContainsKey(key));
				foreach (var value in fast.Values)
					Assert.IsTrue(facit.ContainsValue(value));
			}
		}

		public int GetRandomKey(Dictionary<int, string> facit)
		{
			Assert.IsTrue(facit.Count > 0);

			int idx = PRNG.Next(0, facit.Count);
			int num = 0;
			foreach (var key in facit.Keys)
			{
				if (idx == num)
					return key;
				num++;
			}
			Assert.Fail();
			return -1;
		}
	}
}


<|start_filename|>tests/TimeServiceTests.cs<|end_filename|>
﻿using System;
using Lidgren.Core;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace UnitTests
{
	[TestClass]
	public partial class TimeServiceTests
	{
		[TestMethod]
		public void TestTimeService()
		{
			var wall = TimeService.Wall;

			bool ok;

			var str = TimeService.CompactDuration(1234567.5);
			Assert.AreEqual("342h56m7.5s", str);
			ok = TimeService.TryParseDuration(str, out var back);
			Assert.AreEqual(1234567.5, back);

			Assert.AreEqual("10m", TimeService.CompactDuration(600));
			Assert.AreEqual("10s", TimeService.CompactDuration(10));
			Assert.AreEqual("100ms", TimeService.CompactDuration(0.1));

			ok = TimeService.TryParseDuration("3h2m12s", out var seconds);
			var facit = (3 * 60 * 60);
			facit += (2 * 60);
			facit += 12;
			Assert.IsTrue(ok);
			Assert.AreEqual(facit, seconds);

			ok = TimeService.TryParseDuration("faulty string", out var _);
			Assert.IsFalse(ok);

			TestParseDuration("8", 8);
			TestParseDuration("8s", 8);
			TestParseDuration("-10m", -600);
			TestParseDuration("0m", 0);
			TestParseDuration("1m1s", 61);
			TestParseDuration("3h2m12s", (3 * 60 * 60) + (2 * 60) + 12);
			TestParseDuration("1.5m", 90);
		}

		private static void TestParseDuration(string str, double facit)
		{
			var ok = TimeService.TryParseDuration(str, out var result);
			Assert.IsTrue(ok);
			Assert.AreEqual(facit, result);
		}
	}
}


<|start_filename|>src/Collections/FastList/FastList.Remove.cs<|end_filename|>
﻿#nullable enable
using System;
using System.Runtime.CompilerServices;

namespace Lidgren.Core
{
	public partial class FastList<T>
	{
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void Clear()
		{
			if (RuntimeHelpers.IsReferenceOrContainsReferences<T>())
				this.Span.Clear();
			m_count = 0;
			m_offset = 0;
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void Clear(int minimumCapacity)
		{
			if (m_buffer.Length < minimumCapacity)
				m_buffer = new T[minimumCapacity];
			else if (RuntimeHelpers.IsReferenceOrContainsReferences<T>())
				this.Span.Clear();
			m_count = 0;
			m_offset = 0;
		}

		/// <summary>
		/// Removes first instance of item in list; returns true if found and removed
		/// </summary>
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public bool Remove(in T item)
		{
			var idx = IndexOf(item);
			if (idx == -1)
				return false;
			RemoveAt(idx);
			return true;
		}

		/// <summary>
		/// Removes first instance of item in list; returns true if found and removed; does NOT maintain order of list after removal
		/// </summary>
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public bool RemoveUnordered(in T item)
		{
			var idx = IndexOf(item);
			if (idx == -1)
				return false;
			RemoveAtUnordered(idx);
			return true;
		}

		/// <summary>
		/// Removes item at index; maintaining list order
		/// </summary>
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void RemoveAt(int index)
		{
			CoreException.Assert(index >= 0 && index < m_count);

			int offset = m_offset;
			int countAfterRemove = m_count - 1;
			m_count = countAfterRemove;

			if (index == 0)
			{
				if (RuntimeHelpers.IsReferenceOrContainsReferences<T>())
					m_buffer[offset] = default(T);
				if (countAfterRemove == 0)
					m_offset = 0; // effectively a clear
				else
					m_offset = offset + 1;
				return;
			}

			int copyLen = countAfterRemove - index;
			if (copyLen > 0)
			{
				var buffer = m_buffer;
				for (int i = 0; i < copyLen; i++)
				{
					int c = offset + index + i;
					buffer[c] = buffer[c + 1];
				}
			}
			if (RuntimeHelpers.IsReferenceOrContainsReferences<T>())
				m_buffer[offset + countAfterRemove] = default(T);
		}

		/// <summary>
		/// Removes item at index; not maintaining list order
		/// </summary>
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void RemoveAtUnordered(int index)
		{
			CoreException.Assert(index >= 0 && index < m_count);

			int offset = m_offset;
			int countAfterRemove = m_count--;

			if (index == 0)
			{
				if (countAfterRemove == 0)
					m_offset = 0; // effectively a clear
				else
					m_offset = offset + 1;
				return;
			}

			// simply swap last item into place of removed item
			m_buffer[offset + index] = m_buffer[offset + countAfterRemove];

			if (RuntimeHelpers.IsReferenceOrContainsReferences<T>())
				m_buffer[offset + countAfterRemove] = default;
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void RemoveRange(int index, int count)
		{
			int localSpanCount = m_count;

			if (index + count > localSpanCount)
				CoreException.Throw("Bad range");

			if (count == 0)
				return;

			// clear out the data if necessary
			if (RuntimeHelpers.IsReferenceOrContainsReferences<T>())
				Span.Slice(index, count).Clear();

			m_count -= count;

			if (count == localSpanCount)
			{
				// removing entire span
				m_offset = 0;
				return;
			}

			if (index + count == localSpanCount)
			{
				// removing a tailing part of span
				return;
			}

			if (index == 0)
			{
				// removing first part of span
				m_offset += count;
				return;
			}

			// removing a middle section of span
			// contract latter part
			int copyLen = localSpanCount - (index + count);
			if (copyLen > 0)
			{
				var buffer = m_buffer;
				var offset = m_offset + index;
				for (int i = 0; i < copyLen; i++)
				{
					int c = offset + i;
					buffer[c] = buffer[c + count];
				}
			}
		}

		/// <summary>
		/// Returns all items matching predicate; returns number of items removed
		/// </summary>
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public int RemoveAll(Predicate<T> match)
		{
			int offset = m_offset;
			int count = m_count;

			int idx = 0;
			while (idx < count && !match(m_buffer[offset + idx]))
				idx++;

			if (idx >= count)
				return 0;

			int i = idx + 1;
			while (i < count)
			{
				while (i < count && match(m_buffer[offset + i]))
					i++;
				if (i < count)
					m_buffer[offset + idx++] = m_buffer[offset + i++];
			}
			Array.Clear(m_buffer, offset + idx, count - idx);
			int result = count - idx;
			m_count = idx;
			return result;
		}

		/// <summary>
		/// Creates a new FastList with all matching items; or NULL if nothing matches
		/// </summary>
		public FastList<T>? Find(Predicate<T> match)
		{
			FastList<T>? retval = null;

			int offset = m_offset;
			int count = m_count;

			int idx = 0;
			while (idx < count)
			{
				if (match(m_buffer[offset + idx]) == true)
				{
					if (retval is null)
						retval = new FastList<T>(count - idx);
					retval.Add(m_buffer[offset + idx]);
				}
				idx++;
			}
			return retval;
		}
	}
}


<|start_filename|>src/SpanExtensions.7bit.cs<|end_filename|>
﻿using System;
using System.Buffers.Binary;
using System.Runtime.CompilerServices;

namespace Lidgren.Core
{
	//
	// Variable length integer encoding "7bit encoding"
	//
	// Encodes using 1 byte:    2^0 >= x <  2^7    (0 to 127)  
	// Encodes using 2 bytes:   2^7 >= x < 2^14    (128 - 16383)  
	// Encodes using 3 bytes:  2^14 >= x < 2^21    (16384 - 2097151)  
	// Encodes using 4 bytes:  2^21 >= x < 2^28    (2097152 - 268435455)  
	// Encodes using 5 bytes:  2^28 >= x < 2^35    (268435456 - 34359738367)
	// Encodes using 6 bytes:  2^35 >= x < 2^42    (34359738368 - 4398046511103)
	// Encodes using 7 bytes:  2^42 >= x < 2^49    (4398046511104 - 562949953421311)
	// Encodes using 8 bytes:  2^49 >= x < 2^56    (562949953421312 - 72057594037927935)
	// Encodes using 9 bytes:  2^56 >= x <= 2^64   (72057594037927936 - 18446744073709551615)
	//
	public static partial class SpanExtensions
	{
		/// <summary>
		/// Writes UInt64 as a 7 bit encoded number (variable length: 1 to 9 bytes) and reduces span to remaining data; returns number of bytes written
		/// </summary>
		public static int WriteVariableUInt64(ref this Span<byte> span, ulong value)
		{
			if (value < 0x80)
			{
				span[0] = (byte)value;
				span = span.Slice(1);
				return 1;
			}

			span[0] = (byte)(value | 0x80);
			value >>= 7;

			if (value < 0x80)
			{
				span[1] = (byte)value;
				span = span.Slice(2);
				return 2;
			}

			span[1] = (byte)(value | 0x80);
			value >>= 7;

			if (value < 0x80)
			{
				span[2] = (byte)value;
				span = span.Slice(3);
				return 3;
			}

			span[2] = (byte)(value | 0x80);
			value >>= 7;

			if (value < 0x80)
			{
				span[3] = (byte)value;
				span = span.Slice(4);
				return 4;
			}

			span[3] = (byte)(value | 0x80);
			value >>= 7;

			if (value < 0x80)
			{
				span[4] = (byte)value;
				span = span.Slice(5);
				return 5;
			}

			span[4] = (byte)(value | 0x80);
			value >>= 7;

			if (value < 0x80)
			{
				span[5] = (byte)value;
				span = span.Slice(6);
				return 6;
			}

			span[5] = (byte)(value | 0x80);
			value >>= 7;

			if (value < 0x80)
			{
				span[6] = (byte)value;
				span = span.Slice(7);
				return 7;
			}

			span[6] = (byte)(value | 0x80);
			value >>= 7;

			if (value < 0x80)
			{
				span[7] = (byte)value;
				span = span.Slice(8);
				return 8;
			}

			span[7] = (byte)(value | 0x80);
			value >>= 7;

			span[8] = (byte)value;
			span = span.Slice(9);
			return 9;
		}

		/// <summary>
		/// Writes a byte and reduces span to remaining data
		/// </summary>
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static void WriteByte(ref this Span<byte> span, byte value)
		{
			span[0] = value;
			span = span.Slice(1);
		}

		/// <summary>
		/// Writes bytes and reduces span to remaining data
		/// </summary>
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static void WriteBytes(ref this Span<byte> span, ReadOnlySpan<byte> data)
		{
			data.CopyTo(span);
			span = span.Slice(data.Length);
		}

		/// <summary>
		/// Writes UInt32 as a 7 bit encoded number (variable length: 1 to 5 bytes) and reduces span to remaining data
		/// </summary>
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static int WriteVariableUInt32(ref this Span<byte> span, uint value)
		{
			// TODO: write specialized if performance can be gained
			return WriteVariableUInt64(ref span, value);
		}

		/// <summary>
		/// Writes Int64 as a 7 bit encoded number (variable length: 1 to 9 bytes) and reduces span to remaining data
		/// </summary>
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static int WriteVariableInt64(ref this Span<byte> span, long value)
		{
			// zigzag encode
			var uval = (ulong)((value << 1) ^ (value >> 63));
			return WriteVariableUInt64(ref span, uval);
		}

		/// <summary>
		/// Writes Int64 as a 7 bit encoded number (variable length: 1 to 9 bytes) and reduces span to remaining data
		/// </summary>
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static int WriteVariableInt32(ref this Span<byte> span, int value)
		{
			// zigzag encode
			var uval = (uint)((value << 1) ^ (value >> 31));
			return WriteVariableUInt32(ref span, uval);
		}

		/// <summary>
		/// Reads UInt64 from up to nine bytes and reduces span to remaining data
		/// </summary>
		public static ulong ReadVariableUInt64(ref this ReadOnlySpan<byte> data)
		{
			ulong value = data[0];
			if ((value & 0x80) == 0)
			{
				data = data.Slice(1);
				return value;
			}
			value &= 0x7F;

			ulong chunk = data[1];
			if ((chunk & 0x80) == 0)
			{
				data = data.Slice(2);
				return value | (chunk << 7);
			}
			value |= (chunk & 0x7F) << 7;

			ulong another = data[2];
			if ((another & 0x80) == 0)
			{
				data = data.Slice(3);
				return value | (another << 14);
			}
			value |= (another & 0x7F) << 14;

			chunk = data[3];
			if ((chunk & 0x80) == 0)
			{
				data = data.Slice(4);
				return value | (chunk << 21);
			}
			value |= (chunk & 0x7F) << 21;

			another = data[4];
			if ((another & 0x80) == 0)
			{
				data = data.Slice(5);
				return value | (another << 28);
			}
			value |= (another & 0x7F) << 28;

			chunk = data[5];
			if ((chunk & 0x80) == 0)
			{
				data = data.Slice(6);
				return value | (chunk << 35);
			}
			value |= (chunk & 0x7F) << 35;

			another = data[6];
			if ((another & 0x80) == 0)
			{
				data = data.Slice(7);
				return value | (another << 42);
			}
			value |= (another & 0x7F) << 42;

			chunk = data[7];
			if ((chunk & 0x80) == 0)
			{
				data = data.Slice(8);
				return value | (chunk << 49);
			}
			value |= (chunk & 0x7F) << 49;

			value |= (ulong)data[8] << 56; // full 8 bits in this byte
			data = data.Slice(9);
			return value;
		}

		private const long InvInt64Msb = ~(((long)1) << 63);

		/// <summary>
		/// Reads Int64 from up to nine bytes and reduces span to remaining data
		/// </summary>
		public static long ReadVariableInt64(ref this ReadOnlySpan<byte> data)
		{
			long value = (long)ReadVariableUInt64(ref data);

			// zigzag decode
			return (-(value & 0x01L)) ^ ((value >> 1) & InvInt64Msb);
		}

		/// <summary>
		/// Reads UInt32 from up to five bytes and reduces span to remaining data
		/// </summary>
		public static uint ReadVariableUInt32(ref this ReadOnlySpan<byte> data)
		{
			// TODO: make explicit ReadVariableUInt32()
			return (uint)ReadVariableUInt64(ref data);
		}

		/// <summary>
		/// Reads Int32 from up to five bytes and reduces span to remaining data
		/// </summary>
		public static int ReadVariableInt32(ref this ReadOnlySpan<byte> data)
		{
			long value = (long)ReadVariableUInt64(ref data);

			// zigzag decode
			var signed = (-(value & 0x01L)) ^ ((value >> 1) & InvInt64Msb);
			return (int)signed;
		}
	}
}


<|start_filename|>tests/BitVectorTests.cs<|end_filename|>
﻿using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;

using Lidgren.Core;

namespace UnitTests
{
	[TestClass]
	public class BitVectorTests
	{
		[TestMethod]
		public void TestBitVector()
		{
			var facit = new bool[9 * 64];
			for (int r = 1; r < 8; r++)
			{
				var sz = r * 64;
				var bv = new BitVector(sz);
				facit.AsSpan().Clear();

				for (int op = 0; op < 10000; op++)
				{
					var a = PRNG.Next(0, sz);
					var t = PRNG.Next(0, 14);
					switch (t)
					{
						case 0:
							bv.Clear();
							facit.AsSpan().Clear();
							break;
						case 1:
							bv.Set(a, true);
							facit[a] = true;
							break;
						case 2:
						case 3:
							bv.Set(a);
							facit[a] = true;
							break;
						case 4:
						case 5:
						case 6:
							bv[a] = true;
							facit[a] = true;
							break;
						case 7:
						case 8:
							bv.Clear(a);
							facit[a] = false;
							break;
						case 9:
							bv.Set(a, false);
							facit[a] = false;
							break;
						case 10:
							bv[a] = false;
							facit[a] = false;
							break;
						case 11:
						case 12:
						case 13:
							bv.Flip(a);
							if (facit[a])
								facit[a] = false;
							else
								facit[a] = true;
							break;
					}

					// verify
					var cnt = 0;
					for (int i = 0; i < sz; i++)
					{
						Assert.AreEqual(facit[i], bv[i]);
						if (facit[i])
							cnt++;
					}
					Assert.AreEqual(cnt, bv.CountSetBits());
				}
			}
		}
	}
}


<|start_filename|>src/Text/LinesReader.cs<|end_filename|>
﻿using System;
using System.Buffers;
using System.IO;

namespace Lidgren.Core
{
	/// <summary>
	/// Reads lines without generating strings; splits on \n but omits preceding \r if present
	/// </summary>
	public sealed class LinesReader : IDisposable
	{
		private char[] m_buffer;
		private bool m_bufferCreated;
		private StreamReader m_rdr;

		private int m_start;
		private int m_end; // exclusive

		private bool m_eof;

		public LinesReader(string filename, char[] buffer = null)
		{
			try
			{
				if (buffer is null)
				{
					m_buffer = ArrayPool<char>.Shared.Rent(1024 * 32);
					m_bufferCreated = true;
				}
				else
				{
					m_buffer = buffer;
				}

				var fs = new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete);
				m_rdr = new StreamReader(fs);
			}
			catch
			{
				if (m_rdr != null)
					DisposeUtils.Dispose(ref m_rdr);
				if (m_bufferCreated == true && m_buffer != null)
					ArrayPool<char>.Shared.Return(m_buffer);
				m_buffer = null;
				throw;
			}
		}

		public LinesReader(Stream stream, char[] buffer = null)
		{
			try
			{
				if (buffer is null)
				{
					m_buffer = ArrayPool<char>.Shared.Rent(1024 * 32);
					m_bufferCreated = true;
				}
				else
				{
					m_buffer = buffer;
				}

				m_rdr = new StreamReader(stream);
			}
			catch
			{
				if (m_rdr != null)
					DisposeUtils.Dispose(ref m_rdr);
				if (m_bufferCreated == true && m_buffer != null)
					ArrayPool<char>.Shared.Return(m_buffer);
				m_bufferCreated = false;
				m_buffer = null;
				throw;
			}
		}

		public void Dispose()
		{
			if (m_rdr != null)
				DisposeUtils.Dispose(ref m_rdr);

			if (m_bufferCreated == true)
			{
				var buf = m_buffer;
				m_buffer = null;
				if (buf != null)
					ArrayPool<char>.Shared.Return(buf);
			}
		}

		public bool ReadLine(out ReadOnlySpan<char> line)
		{
			if (m_eof)
			{
				line = default;
				return false;
			}

			// find end of line
			int idx = m_start;
			int end = m_end;
			for (; ; )
			{
				if (idx >= end)
				{
					// end of data, but no newline yet
					int curLen = end - idx;
					int wasRead = ReadBuffer();
					end = m_end;
					if (wasRead == 0)
					{
						// last line; without newline
						line = new ReadOnlySpan<char>(m_buffer, 0, end);
						m_eof = true;
						return end > 0;
					}

					// continue reading
					idx = curLen;
				}

				if (m_buffer[idx] == '\n')
					break;
				idx++;
			}

			int len = idx - m_start;

			// remove \r if \r\n
			if (len > 0 && m_buffer[m_start + len - 1] == '\r')
				line = new ReadOnlySpan<char>(m_buffer, m_start, len - 1);
			else
				line = new ReadOnlySpan<char>(m_buffer, m_start, len);
			m_start += (len + 1); // step past newline char also
			return true;
		}

		private int ReadBuffer()
		{
			int len = m_end - m_start;
			if (len > 0)
			{
				// move back data
				var src = new ReadOnlySpan<char>(m_buffer, m_start, len);
				src.CopyTo(m_buffer);
			}
			m_start = 0;
			m_end = len;

			int rem = m_buffer.Length - len;
			int wasRead = m_rdr.Read(new Span<char>(m_buffer, len, rem));
			m_end += wasRead;
			return wasRead;
		}
	}
}


<|start_filename|>src/Hash/Hasher.cs<|end_filename|>
﻿#nullable enable
using System;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

namespace Lidgren.Core
{
	/// <summary>
	/// Incremental hashing based on MurmurHash64A by <NAME>
	/// </summary>
	[StructLayout(LayoutKind.Auto)]
	public ref struct Hasher
	{
		private const ulong M = 0xc6a4a7935bd1e995ul;
		private const int R = 47;

		private ulong m_hash;
		private ulong m_unit;
		private int m_bitCount; // how many unconsumed bits are stored in m_unit

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static Hasher Create()
		{
			return new Hasher(M);
		}

		public readonly Hasher Clone()
		{
			Hasher retval;
			retval.m_hash = m_hash;
			retval.m_unit = m_unit;
			retval.m_bitCount = m_bitCount;
			return retval;
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public Hasher(in ulong seed)
		{
			m_hash = seed;
			m_unit = 0;
			m_bitCount = 0;
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void Reset(in ulong seed = M)
		{
			m_hash = seed;
			m_unit = 0;
			m_bitCount = 0;
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		private void Consume(ulong data)
		{
			unchecked
			{
				data *= M;
				data ^= data >> R;
				data *= M;

				ulong hash = m_hash ^ data;
				hash *= M;
				m_hash = hash;
			}
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		private void ConsumeUnit()
		{
			unchecked
			{
				var unit = m_unit * M;
				unit ^= unit >> R;
				unit *= M;

				var hash = m_hash ^ unit;
				hash *= M;

				m_unit = unit;
				m_hash = hash;
			}
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void Add(byte data)
		{
			unchecked
			{
				int bc = m_bitCount;
				m_unit |= ((ulong)data << bc);
				bc = (bc + 8) & 63;
				if (bc == 0)
				{
					ConsumeUnit();
					m_unit = 0;
					m_bitCount = 0;
				}
				m_bitCount = bc;
			}
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void Add(short value)
		{
			Add((ushort)value);
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void Add(int value)
		{
			Add((uint)value);
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void Add(float value)
		{
			Add(SingleUIntUnion.ReinterpretCast(value));
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void Add(Vector2 value)
		{
			SinglesULongUnion union;
			union.ULongValue = 0;
			union.SingleValue0 = value.X;
			union.SingleValue1 = value.Y;
			Add(union.ULongValue);
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void Add(Vector3 value)
		{
			SingleUIntUnion union;
			union.UIntValue = 0;
			union.SingleValue = value.X;
			Add(union.UIntValue);
			union.SingleValue = value.Y;
			Add(union.UIntValue);
			union.SingleValue = value.Z;
			Add(union.UIntValue);
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void Add(Vector4 value)
		{
			SinglesULongUnion union;
			union.ULongValue = 0;
			union.SingleValue0 = value.X;
			union.SingleValue1 = value.Y;
			Add(union.ULongValue);
			union.SingleValue0 = value.Z;
			union.SingleValue1 = value.W;
			Add(union.ULongValue);
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void Add(Quaternion value)
		{
			// TODO: check if using AddStructBytes<Quaternion> is faster
			SinglesULongUnion union;
			union.ULongValue = 0;
			union.SingleValue0 = value.X;
			union.SingleValue1 = value.Y;
			Add(union.ULongValue);
			union.SingleValue0 = value.Z;
			union.SingleValue1 = value.W;
			Add(union.ULongValue);
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void Add(float one, float two)
		{
			SinglesULongUnion union;
			union.ULongValue = 0;
			union.SingleValue0 = one;
			union.SingleValue1 = two;
			Add(union.ULongValue);
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void Add(double value)
		{
			Add(DoubleULongUnion.ReinterpretCast(value));
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void Add(ushort data)
		{
			unchecked
			{
				if (m_bitCount == 56)
				{
					// special case; only room for one more byte in m_unit
					m_unit |= ((ulong)data << 56); // store lower byte; trunc top byte
					ConsumeUnit();
					m_unit = ((ulong)data >> 8); // store top byte
					m_bitCount = 8;
					return;
				}

				m_unit |= ((ulong)data << m_bitCount);
				m_bitCount = (m_bitCount + 16) & 63;
				if (m_bitCount == 0)
				{
					ConsumeUnit();
					m_unit = 0;
					m_bitCount = 0;
				}
			}
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void Add(uint data)
		{
			unchecked
			{
				int bc = m_bitCount;
				m_unit |= ((ulong)data << bc); // fill entire or trunc top
				if (bc < 32)
				{
					m_bitCount += 32;
					return;
				}

				ConsumeUnit();

				if (bc == 32)
				{
					m_unit = 0;
					m_bitCount = 0;
					return;
				}

				int nbc = 64 - bc;
				m_bitCount = nbc;
				m_unit = data >> nbc;
			}
		}

		public void Add(byte[] data, int offset, int length)
		{
			int end = offset + length;
			while (m_bitCount != 0 && offset != end)
				Add(data[offset++]);

			while (end - offset >= 8)
			{
				// add full ulongs
				var span = new ReadOnlySpan<byte>(data, offset, 8);
				ulong add = MemoryMarshal.Read<ulong>(span);
				Consume(add);
				offset += 8;
			}

			// add remaining bytes
			while (offset != end)
				Add(data[offset++]);
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void Add(ReadOnlySpan<int> data)
		{
			Add(MemoryMarshal.Cast<int, byte>(data));
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void Add(ReadOnlySpan<uint> data)
		{
			Add(MemoryMarshal.Cast<uint, byte>(data));
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void Add(ReadOnlySpan<ushort> data)
		{
			Add(MemoryMarshal.Cast<ushort, byte>(data));
		}

		[MethodImpl(MethodImplOptions.AggressiveOptimization)]
		public void Add(ReadOnlySpan<byte> data)
		{
			unchecked
			{
				// add bytes until unit is empty
				var bc = m_bitCount;
				while (bc != 0)
				{
					if (data.Length < 1)
					{
						m_bitCount = bc;
						return;
					}
					m_unit |= ((ulong)data[0] << bc);
					data = data.Slice(1);
					bc = (bc + 8) & 63;
					if (bc == 0)
					{
						ConsumeUnit();
						m_unit = 0;
						m_bitCount = 0;
						break;
					}
				}

				int numChunks = Math.DivRem(data.Length, 8, out var remainder);
				int fastBytes = data.Length - remainder;
				if (numChunks > 0)
				{
					// add full ulongs
					ulong hash = m_hash;
					var chunks = MemoryMarshal.Cast<byte, ulong>(data.Slice(0, fastBytes));
					CoreException.Assert(numChunks == chunks.Length);
					while (chunks.Length > 0)
					{
						ulong bits = chunks[0];
						bits *= M;
						bits ^= bits >> R;
						bits *= M;
						hash ^= bits;
						hash *= M;
						chunks = chunks.Slice(1);
					}
					m_hash = hash;
				}

				// add remaining bytes; they will fit into unit
				CoreException.Assert(m_bitCount == 0);

				data = data.Slice(fastBytes);
				CoreException.Assert(data.Length >= 0 && data.Length <= 7);
				m_bitCount = data.Length * 8;
				switch (data.Length)
				{
					case 0:
						return;
					case 1:
						m_unit = data[0];
						return;
					case 2:
						m_unit = data[0] | (ulong)data[1] << 8;
						return;
					case 3:
						m_unit = data[0] | (ulong)data[1] << 8 | (ulong)data[2] << 16;
						return;
					case 4:
						m_unit = data[0] | (ulong)data[1] << 8 | (ulong)data[2] << 16 | (ulong)data[3] << 24;
						return;
					case 5:
						m_unit = data[0] | (ulong)data[1] << 8 | (ulong)data[2] << 16 | (ulong)data[3] << 24 | (ulong)data[4] << 32;
						return;
					case 6:
						m_unit = data[0] | (ulong)data[1] << 8 | (ulong)data[2] << 16 | (ulong)data[3] << 24 | (ulong)data[4] << 32 | (ulong)data[5] << 40;
						return;
					case 7:
						m_unit = data[0] | (ulong)data[1] << 8 | (ulong)data[2] << 16 | (ulong)data[3] << 24 | (ulong)data[4] << 32 | (ulong)data[5] << 40 | (ulong)data[6] << 48;
						return;
				}
			}
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void Add(bool value)
		{
			Add(value ? (byte)0b11001100 : (byte)0b10101010); // arbitrarily chosen numbers
		}

		[MethodImpl(MethodImplOptions.AggressiveOptimization)]
		public void Add(ReadOnlySpan<char> str)
		{
			if (str.Length <= 0)
			{
				Add((byte)0);
				return;
			}

			var bytes = MemoryMarshal.Cast<char, byte>(str);
			Add(bytes);
		}

		public void AddStructBytes<T>(ref T item) where T : unmanaged
		{
			int sizeInBytes = Unsafe.SizeOf<T>();
			ReadOnlySpan<T> span = MemoryMarshal.CreateSpan<T>(ref item, sizeInBytes);
			ReadOnlySpan<byte> bytes = MemoryMarshal.AsBytes<T>(span);
			Add(bytes);
		}

		public void AddStructBytes<T>(ReadOnlySpan<T> items) where T : unmanaged
		{
			var bytes = MemoryMarshal.Cast<T, byte>(items);
			Add(bytes);
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void AddLowerInvariant(ReadOnlySpan<char> str)
		{
			if (str.Length < 1)
			{
				Add((byte)0);
				return;
			}
			foreach (var c in str)
				Add((ushort)Char.ToLowerInvariant(c));
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void AddLower(ReadOnlySpan<char> str)
		{
			if (str.Length < 1)
			{
				Add((byte)0);
				return;
			}
			foreach (var c in str)
				Add((ushort)Char.ToLower(c));
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void Add(ulong data)
		{
			unchecked
			{
				int bc = m_bitCount;
				if (bc == 0)
				{
					Consume(data);
					return;
				}

				// fill using lower bytes
				m_unit |= (data << bc);
				ConsumeUnit();

				// replace unit with upper bytes
				// bitcount remains the same since we added a full 64 bit value
				m_unit = (data >> (64 - bc));
			}
		}

		/// <summary>
		/// Use this if the data you've added so far aligns to 64 bits
		/// </summary>
		public void AddAligned64(ulong data)
		{
			Consume(data);
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public ulong Finalize64()
		{
			unchecked
			{
				ulong hash = m_hash;
				if (m_bitCount != 0)
				{
					hash ^= m_unit;
					hash *= M;
				}
				hash ^= hash >> R;
				hash *= M;
				hash ^= hash >> R;
				m_hash = hash;
				return hash;
			}
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public uint Finalize32()
		{
			return HashUtil.XorFold64To32(Finalize64());
		}
	}
}


<|start_filename|>src/StdOut.cs<|end_filename|>
﻿#nullable enable
using System;
using System.Diagnostics;

namespace Lidgren.Core
{
	/// <summary>
	/// Console output utility. Supports passing span of char and coloring.
	/// </summary>
	public static class StdOut
	{
		// Console is thread safe; but to ensure coloring is atomic we use a lock
		private static readonly object s_lock = new object();

		public static void Flush()
		{
			lock (s_lock)
				Console.Out.Flush();
		}

		/// <summary>
		/// Writes to console
		/// </summary>
		public static void WriteLine(ReadOnlySpan<char> text)
		{
			lock (s_lock)
				Console.Out.WriteLine(text);
		}

		/// <summary>
		/// Writes to console
		/// </summary>
		public static void WriteLine()
		{
			lock (s_lock)
				Console.Out.WriteLine();
		}

		/// <summary>
		/// Writes to console
		/// </summary>
		public static void Write(ReadOnlySpan<char> text)
		{
			lock (s_lock)
				Console.Out.Write(text);
		}

		/// <summary>
		/// Writes to console, only in DEBUG configuration
		/// </summary>
		[Conditional("DEBUG")]
		public static void DebugLine(ReadOnlySpan<char> text)
		{
			WriteLine(text);
		}

		/// <summary>
		/// Writes to console, only in DEBUG configuration
		/// </summary>
		[Conditional("DEBUG")]
		public static void Debug(ReadOnlySpan<char> text)
		{
			Write(text);
		}

		/// <summary>
		/// Writes to console with a particular foreground color
		/// </summary>
		public static void WriteLine(ReadOnlySpan<char> text, ConsoleColor color)
		{
			lock (s_lock)
			{
				Console.ForegroundColor = color;
				Console.Out.WriteLine(text);
				Console.ResetColor();
			}
		}

		/// <summary>
		/// Writes to console with a particular foreground color
		/// </summary>
		public static void Write(ReadOnlySpan<char> text, ConsoleColor color)
		{
			lock (s_lock)
			{
				Console.ForegroundColor = color;
				Console.Out.Write(text);
				Console.ResetColor();
			}
		}

		private const string k_escape = "\u001b[";
		private const string k_escapeReset = "\u001b[m";
		private const string k_escapeForegroundColor = "\u001b[38;2;";
		private const string k_escapeBackgroundColor = "\u001b[48;2;";
		private const string k_escapeClearScreen = "\u001b[2Jm\u001b[H";

		public static void ClearScreen(ReadOnlySpan<char> hexBackgroundColor = default)
		{
			lock (s_lock)
			{
				if (hexBackgroundColor != default)
				{
					Span<char> buffer = stackalloc char[64];
					Span<char> work = buffer;

					AppendAnsiColorCode(k_escapeBackgroundColor, hexBackgroundColor, ref work);
					var code = buffer.Slice(0, buffer.Length - work.Length);

					Console.Out.Write(code);
				}

				Console.Out.Write(k_escapeClearScreen);
			}
		}

		/// <summary>
		/// Creates a string with prefixed ansi color codes and postfixed with reset code
		/// </summary>
		public static string AnsiColor(ReadOnlySpan<char> text, ReadOnlySpan<char> hexForegroundColor, ReadOnlySpan<char> hexBackgroundColor = default)
		{
			Span<char> buffer = stackalloc char[256];
			Span<char> work = buffer;

			int resetLen = k_escapeReset.Length;

			// foreground
			AppendAnsiColorCode(k_escapeForegroundColor, hexForegroundColor, ref work);

			if (hexBackgroundColor != default)
				AppendAnsiColorCode(k_escapeBackgroundColor, hexBackgroundColor, ref work);

			int codeLen = buffer.Length - work.Length;

			if (work.Length >= text.Length + resetLen)
			{
				// fits in stack allocation
				text.CopyTo(work.Slice(0, text.Length));
				k_escapeReset.AsSpan().CopyTo(work.Slice(text.Length, resetLen));
				return new string(buffer.Slice(0, codeLen + text.Length + resetLen));
			}

			var code = buffer.Slice(0, codeLen);

			return code.ToString() + text.ToString() + k_escapeReset;
		}

		/// <summary>
		/// Writes to console with a particular foreground color and optionally background color
		/// </summary>
		public static void WriteAnsiLine(ReadOnlySpan<char> text, ReadOnlySpan<char> hexForegroundColor, ReadOnlySpan<char> hexBackgroundColor = default)
		{
			Span<char> buffer = stackalloc char[64];
			Span<char> work = buffer;

			// foreground
			AppendAnsiColorCode(k_escapeForegroundColor, hexForegroundColor, ref work);

			if (hexBackgroundColor != default)
				AppendAnsiColorCode(k_escapeBackgroundColor, hexBackgroundColor, ref work);

			var code = buffer.Slice(0, buffer.Length - work.Length);

			lock (s_lock)
			{
				Console.Out.Write(code);
				Console.Out.Write(text);
				Console.Out.WriteLine(k_escapeReset);
			}
		}

		/// <summary>
		/// Writes to console with a particular foreground color and optionally background color
		/// </summary>
		public static void WriteAnsi(ReadOnlySpan<char> text, ReadOnlySpan<char> hexForegroundColor, ReadOnlySpan<char> hexBackgroundColor = default)
		{
			Span<char> buffer = stackalloc char[64];
			Span<char> work = buffer;

			// foreground
			AppendAnsiColorCode(k_escapeForegroundColor, hexForegroundColor, ref work);

			if (hexBackgroundColor != default)
				AppendAnsiColorCode(k_escapeBackgroundColor, hexBackgroundColor, ref work);

			var code = buffer.Slice(0, buffer.Length - work.Length);

			lock (s_lock)
			{
				Console.Out.Write(code);
				Console.Out.Write(text);
				Console.Out.Write(k_escapeReset);
			}
		}

		private static void AppendAnsiColorCode(ReadOnlySpan<char> codePrefix, ReadOnlySpan<char> hexColor, ref Span<char> work)
		{
			var color = Color.FromHex(hexColor);
			codePrefix.CopyTo(work);
			work = work.Slice(codePrefix.Length);

			// red
			color.R.TryFormat(work, out int written);
			work[written] = ';';
			work = work.Slice(written + 1);

			// green
			color.G.TryFormat(work, out written);
			work[written] = ';';
			work = work.Slice(written + 1);

			// blue
			color.B.TryFormat(work, out written);
			work[written] = 'm';
			work = work.Slice(written + 1);

			return;
		}
	}
}


<|start_filename|>src/Timing/ChromeTraceTimingConsumer.cs<|end_filename|>
﻿#nullable enable
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;

namespace Lidgren.Core
{
	public class ChromeTraceTimingConsumer : IDisposable
	{
		private StreamWriter? m_writer;
		private readonly HashSet<TimingThread> m_seen = new HashSet<TimingThread>(32);

		private double m_invFrequencyToMicros;

		public ChromeTraceTimingConsumer(string filename)
		{
			//m_invFrequencyToMicros = 1000000.0 / (double)Stopwatch.Frequency;
			m_invFrequencyToMicros = 1000000000.0 / (double)Stopwatch.Frequency;

			var fs = new FileStream(filename, FileMode.Create, FileAccess.Write, FileShare.ReadWrite | FileShare.Delete);
			m_writer = new StreamWriter(fs);
			m_writer.WriteLine("{ \"traceEvents\": [");
			m_writer.Flush();
			TimingService.AddListener(Flush);
		}

		private const string k_namePart = ",{\"name\":\"";

		private void Flush(TimingThread thread, TimingEntry[] entries, int count)
		{
			var span = entries.AsSpan(0, count);

			var writer = m_writer;
			if (writer == null)
				return;

			lock (m_seen)
			{
				if (m_seen.Contains(thread) == false)
				{
					// output thread name to chrome trace
					// we can affort to do this inefficiently since it's only done once per thread
					writer.WriteLine(", { \"name\": \"thread_name\", \"ph\": \"M\", \"pid\": 0, \"tid\": " + thread.Index.ToString() + ", \"args\": { \"name\": \"" + thread.Name + "\" } }");
					m_seen.Add(thread);
				}
			}

			var invFrequencyToMicros = m_invFrequencyToMicros;
			var ti = thread.Index;

			Span<char> buf = stackalloc char[256];
			var bdr = new FixedStringBuilder(buf);
			bdr.Append(k_namePart);
			for (int i = 0; i < span.Length; i++)
			{
				bdr.Length = k_namePart.Length; // keep name part only

				ref readonly var entry = ref span[i];

				bdr.Append(entry.Name);

				// add start time
				bdr.Append("\",\"ph\":\"X\",\"ts\":");
				ulong startMicros = (ulong)((double)entry.Start * invFrequencyToMicros);
				bdr.Append(startMicros);

				// add duration
				bdr.Append(",\"dur\":");
				ulong durMicros = (ulong)((double)entry.Duration * invFrequencyToMicros);
				if (durMicros <= 0)
					durMicros = 1;
				bdr.Append(durMicros);

				// add thread index
				bdr.Append(",\"pid\":0,\"tid\":");
				bdr.Append(ti);

				// add end
				bdr.Append('}');

				// write line to stream
				writer.WriteLine(bdr.ReadOnlySpan);
			}
		}

		public void Dispose()
		{
			var writer = m_writer;
			m_writer = null;
			if (writer != null)
			{
				writer.WriteLine("] }");
				var fs = writer.BaseStream as FileStream;
				if (fs != null)
					fs.Flush(true);
				writer.Dispose();
			}
		}
	}
}


<|start_filename|>src/JobService/JobWorker.cs<|end_filename|>
﻿using System;
using System.Threading;

namespace Lidgren.Core
{
	internal enum JobWorkerState
	{
		Run,
		TimingFlushTriggered,
		Shutdown
	}

	internal class JobWorker
	{
		private Thread m_thread;
		private JobWorkerState m_state;
		private string m_name;

		public int Index { get; private set; }

		[ThreadStatic]
		private static JobWorker s_workerForThread;

		/// <summary>
		/// Get JobWorker associated with current thread; if any
		/// </summary>
		public static JobWorker WorkerForThread => s_workerForThread;

#if DEBUG
		public Job CurrentJob;
#endif
		public string Name => m_name;

		internal void SetState(JobWorkerState state)
		{
			if (m_state == JobWorkerState.Shutdown)
				return;
			m_state = state;
		}

		public JobWorker(int index)
		{
			Index = index;

			m_thread = new Thread(new ThreadStart(Run));
			m_thread.IsBackground = true;
			m_name = "JobWorker#" + index.ToString();
			m_thread.Name = m_name;
			m_thread.Start();
		}

		public override string ToString() => m_name;

		private void Run()
		{
			var tt = TimingThread.Instance; // init this thread

			// set thread local
			s_workerForThread = this;

			int numTotalWorkers = JobService.WorkersCount;

			for (; ; )
			{
				if (m_state == JobWorkerState.Shutdown)
				{
					TimingThread.Instance.InternalFlush();
					return; // done
				}

				if (m_state == JobWorkerState.TimingFlushTriggered)
					TimingThread.Instance.InternalFlush();

				if (JobService.ExecuteAnyJob(this))
					continue;

				// wait for a job to be queued (may give false positives but...)
				int numIdle = Interlocked.Increment(ref JobService.m_idleWorkers);
				if (numIdle < numTotalWorkers) // at least one worker should always be awake
					JobService.JobWait.WaitOne();
				numIdle = Interlocked.Decrement(ref JobService.m_idleWorkers);
			}
		}
	}
}


<|start_filename|>src/Text/FixedStringBuilder.cs<|end_filename|>
﻿using System;
using System.Runtime.CompilerServices;

namespace Lidgren.Core
{
	/// <summary>
	/// Fixed size stack allocated string builder backed by (potentially) stack based memory
	/// 
	/// Usage:
	/// Span<char> backing = stackalloc char[64];
	/// var bdr = new ValueStringBuilder(backing);
	/// bdr.Append(...
	/// ... then use .Result or .ToString() to get results
	///
	/// Will throw exception on overflow.
	/// </summary>
	public ref struct FixedStringBuilder
	{
		private static readonly bool s_crlf = Environment.NewLine.Equals("\n", StringComparison.Ordinal) ? false : true;

		private Span<char> m_buffer;
		private Span<char> m_remaining;
		
		public int Length
		{
			readonly get { return m_buffer.Length - m_remaining.Length; }
			set { m_remaining = m_buffer.Slice(value); }
		}

		public readonly Span<char> Span => m_buffer.Slice(0, Length);
		public readonly ReadOnlySpan<char> ReadOnlySpan => m_buffer.Slice(0, Length);

		public FixedStringBuilder(Span<char> buffer)
		{
			m_buffer = buffer;
			m_remaining = buffer;
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void Clear()
		{
			m_remaining = m_buffer;
		}

		// advances span
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		private void NewLine(ref Span<char> span)
		{
			if (s_crlf)
			{
				span[0] = '\r';
				span[1] = '\n';
				span = span.Slice(2);
				return;
			}
			span[0] = '\n';
			span = span.Slice(1);
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void AppendLine()
		{
			NewLine(ref m_remaining);
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void AppendLine(ReadOnlySpan<char> str)
		{
			var rem = m_remaining;
			str.CopyTo(rem);
			rem = rem.Slice(str.Length);
			NewLine(ref rem);
			m_remaining = rem;
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void Append(ReadOnlySpan<char> str)
		{
			str.CopyTo(m_remaining);
			m_remaining = m_remaining.Slice(str.Length);
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void Append(char c)
		{
			m_remaining[0] = c;
			m_remaining = m_remaining.Slice(1);
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void Append(bool value)
		{
			bool ok = value.TryFormat(m_remaining, out int written);
			CoreException.Assert(ok);
			m_remaining = m_remaining.Slice(written);
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void Append(int value)
		{
			bool ok = value.TryFormat(m_remaining, out int written);
			CoreException.Assert(ok);
			m_remaining = m_remaining.Slice(written);
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void Append(uint value)
		{
			bool ok = value.TryFormat(m_remaining, out int written);
			CoreException.Assert(ok);
			m_remaining = m_remaining.Slice(written);
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void Append(long value)
		{
			bool ok = value.TryFormat(m_remaining, out int written);
			CoreException.Assert(ok);
			m_remaining = m_remaining.Slice(written);
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void Append(ulong value)
		{
			bool ok = value.TryFormat(m_remaining, out int written);
			CoreException.Assert(ok);
			m_remaining = m_remaining.Slice(written);
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void Append(float value)
		{
			bool ok = value.TryFormat(m_remaining, out int written);
			CoreException.Assert(ok);
			m_remaining = m_remaining.Slice(written);
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void Append(double value)
		{
			bool ok = value.TryFormat(m_remaining, out int written);
			CoreException.Assert(ok);
			m_remaining = m_remaining.Slice(written);
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void AppendFormat(ReadOnlySpan<char> format, int value)
		{
			bool ok = value.TryFormat(m_remaining, out int written, format: format);
			CoreException.Assert(ok);
			m_remaining = m_remaining.Slice(written);
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void AppendFormat(ReadOnlySpan<char> format, float value)
		{
			bool ok = value.TryFormat(m_remaining, out int written, format: format);
			CoreException.Assert(ok);
			m_remaining = m_remaining.Slice(written);
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void AppendFormat(ReadOnlySpan<char> format, double value)
		{
			bool ok = value.TryFormat(m_remaining, out int written, format: format);
			CoreException.Assert(ok);
			m_remaining = m_remaining.Slice(written);
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void AppendFormat(ReadOnlySpan<char> format, float value, IFormatProvider provider)
		{
			bool ok = value.TryFormat(m_remaining, out int written, format: format, provider: provider);
			CoreException.Assert(ok);
			m_remaining = m_remaining.Slice(written);
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void AppendFormat(ReadOnlySpan<char> format, double value, IFormatProvider provider)
		{
			bool ok = value.TryFormat(m_remaining, out int written, format: format, provider: provider);
			CoreException.Assert(ok);
			m_remaining = m_remaining.Slice(written);
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void AppendFormat(ReadOnlySpan<char> format, uint value)
		{
			bool ok = value.TryFormat(m_remaining, out int written, format: format);
			CoreException.Assert(ok);
			m_remaining = m_remaining.Slice(written);
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void AppendFormat(ReadOnlySpan<char> format, ushort value)
		{
			bool ok = value.TryFormat(m_remaining, out int written, format: format);
			CoreException.Assert(ok);
			m_remaining = m_remaining.Slice(written);
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void AppendFormat(ReadOnlySpan<char> format, ulong value)
		{
			bool ok = value.TryFormat(m_remaining, out int written, format: format);
			CoreException.Assert(ok);
			m_remaining = m_remaining.Slice(written);
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void AppendFormat(ReadOnlySpan<char> format, byte value)
		{
			bool ok = value.TryFormat(m_remaining, out int written, format: format);
			CoreException.Assert(ok);
			m_remaining = m_remaining.Slice(written);
		}

		/// <summary>
		/// Returns number of characters replaced
		/// </summary>
		public int Replace(char oldChar, char newChar)
		{
			var content = this.Span;
			int retval = 0;
			for (int i = 0; i < content.Length; i++)
			{
				if (content[i] == oldChar)
				{
					content[i] = newChar;
					retval++;
				}
			}
			return retval;
		}

		/// <summary>
		/// Returns number of substitutions
		/// </summary>
		public int Replace(string oldValue, string newValue)
		{
			int retval = 0;

			if (oldValue.Length == newValue.Length)
			{
				// in-place replacement
				var span = this.Span;
				for (; ; )
				{
					var idx = span.IndexOf(oldValue);
					if (idx == -1)
						break;
					newValue.AsSpan().CopyTo(span.Slice(idx, newValue.Length));
					retval++;
					span = span.Slice(idx + 1); // avoid infinite substitutions
				}
				return retval;
			}

			for (; ; )
			{
				var span = this.Span;
				var idx = span.IndexOf(oldValue);
				if (idx == -1)
					break;
				Remove(idx, oldValue.Length);
				Insert(idx, newValue);
				retval++;
			}
			return retval;
		}

		public void Insert(int index, ReadOnlySpan<char> value)
		{
			var curLen = this.Length;
			if (index == curLen)
			{
				// just append
				value.CopyTo(m_remaining);
				m_remaining = m_remaining.Slice(value.Length);
				return;
			}

			if (index == 0)
			{
				this.ReadOnlySpan.CopyTo(m_buffer.Slice(value.Length));
				value.CopyTo(m_buffer.Slice(0, value.Length));
				m_remaining = m_buffer.Slice(curLen + value.Length);
				return;
			}

			// in the middle
			var tail = ReadOnlySpan.Slice(index);
			tail.CopyTo(m_buffer.Slice(index + value.Length));
			value.CopyTo(m_buffer.Slice(index));
			m_remaining = m_buffer.Slice(curLen + value.Length);
		}

		public void Remove(int index, int count)
		{
			int curLen = this.Length;
			int tail = curLen - (index + count);
			CoreException.Assert(tail >= 0);
			m_remaining = m_buffer.Slice(curLen - count);
			if (tail == 0)
				return;
			m_buffer.Slice(index + count, tail).CopyTo(m_buffer.Slice(index, tail));
		}

		public override int GetHashCode()
		{
			return (int)HashUtil.Hash32(ReadOnlySpan);
		}

		public override string ToString()
		{
			return ReadOnlySpan.ToString();
		}
	}
}


<|start_filename|>src/Timing/Timing.cs<|end_filename|>
﻿using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;

namespace Lidgren.Core
{
	/// <summary>
	/// Example usage: using var _ = new Timing("myScopeName");
	/// </summary>
	public ref struct Timing
	{
		public string Name;
		public long Started;

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public Timing(string name)
		{
			CoreException.Assert(string.IsNullOrWhiteSpace(name) == false);
			Name = name;
			Started = Stopwatch.GetTimestamp();
		}

		/// <summary>
		/// No need/possibility to implement IDisposable; but compiler magic will call this anyway!
		/// </summary>
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void Dispose()
		{
			long now = Stopwatch.GetTimestamp();
			CoreException.Assert(now >= Started);
			long duration = now - Started;
			if (duration > 0 && duration < int.MaxValue)
				TimingThread.Instance.AddEntry(Name, Started, (int)duration);
		}
	}
}


<|start_filename|>src/DisposeUtils.cs<|end_filename|>
﻿#nullable enable
using System;

namespace Lidgren.Core
{
	public static class DisposeUtils
	{
		/// <summary>
		/// Only use if Dispose(ref ob) cannot be used
		/// </summary>
		public static void Dispose(object ob)
		{
			var copy = ob as IDisposable;
			if (copy != null)
				copy.Dispose();
		}

		/// <summary>
		/// If non-null; calls dispose and nulls this reference. Locks on 'this' if no lock object provided, concurrent safe, will dispose exactly one time.
		/// </summary>
		public static bool Dispose<T>(ref T? ob, object? lockObject = null) where T : class, IDisposable
		{
			var myRef = ob;

			if (lockObject == null)
				lockObject = myRef;

			if (myRef != null)
			{
#pragma warning disable CS8602 // Dereference of a possibly null reference.
				lock (lockObject)
#pragma warning restore CS8602 // Dereference of a possibly null reference.
				{
					if (ob != myRef)
						return false; // someone beat us to it
					try
					{
						ob.Dispose();
						return true;
					}
					catch
					{
						return false;
					}
					finally
					{
						ob = null;
					}
				}
			}
			return false;
		}
	}
}



<|start_filename|>tests/FileCompareTests.cs<|end_filename|>
﻿using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;

using Lidgren.Core;
using System.Reflection;

namespace UnitTests
{
	[TestClass]
	public class FileCompareTests
	{
		[TestMethod]
		public void TextFileCompare()
		{
			// get this assembly file
			var assemblyFile = Assembly.GetExecutingAssembly().Location;

			Assert.AreEqual(FileCompare.Result.SameFile, FileCompare.AreEqual(assemblyFile, assemblyFile));
			Assert.AreEqual(FileCompare.Result.SameFile, FileCompare.AreEqual("FileOne.txt", "FileOne.txt"));
			Assert.AreEqual(FileCompare.Result.Identical, FileCompare.AreEqual("FileOne.txt", "FileTwo.txt"));
			Assert.AreEqual(FileCompare.Result.Different, FileCompare.AreEqual("FileOne.txt", assemblyFile));
			Assert.AreEqual(FileCompare.Result.File2Missing, FileCompare.AreEqual("FileOne.txt", "xxx"));
			Assert.AreEqual(FileCompare.Result.File1Missing, FileCompare.AreEqual("xxx", "FileOne.txt"));
			Assert.AreEqual(FileCompare.Result.BothFilesMissing, FileCompare.AreEqual("xxx", "yyy"));
			Assert.AreEqual(FileCompare.Result.BothFilesMissing, FileCompare.AreEqual("xxx", "xxx"));
			Assert.AreEqual(FileCompare.Result.File1Missing, FileCompare.AreEqual(null, "FileOne.txt"));

			int bufSize = 4096;
			Assert.AreEqual(FileCompare.Result.SameFile, FileCompare.AreEqual(assemblyFile, assemblyFile, bufSize));
			Assert.AreEqual(FileCompare.Result.SameFile, FileCompare.AreEqual("FileOne.txt", "FileOne.txt", bufSize));
			Assert.AreEqual(FileCompare.Result.Identical, FileCompare.AreEqual("FileOne.txt", "FileTwo.txt", bufSize));
			Assert.AreEqual(FileCompare.Result.Different, FileCompare.AreEqual("FileOne.txt", assemblyFile, bufSize));
			Assert.AreEqual(FileCompare.Result.File2Missing, FileCompare.AreEqual("FileOne.txt", "xxx", bufSize));
			Assert.AreEqual(FileCompare.Result.File1Missing, FileCompare.AreEqual("xxx", "FileOne.txt", bufSize));
			Assert.AreEqual(FileCompare.Result.BothFilesMissing, FileCompare.AreEqual("xxx", "yyy", bufSize));
			Assert.AreEqual(FileCompare.Result.BothFilesMissing, FileCompare.AreEqual("xxx", "xxx", bufSize));
			Assert.AreEqual(FileCompare.Result.File1Missing, FileCompare.AreEqual(null, "FileOne.txt", bufSize));

			var buffer = new byte[8192];
			var b1 = buffer.AsSpan(0, 4096);
			var b2 = buffer.AsSpan(4096);
			Assert.AreEqual(FileCompare.Result.SameFile, FileCompare.AreEqual(assemblyFile, assemblyFile, b1, b2));
			Assert.AreEqual(FileCompare.Result.SameFile, FileCompare.AreEqual("FileOne.txt", "FileOne.txt", b1, b2));
			Assert.AreEqual(FileCompare.Result.Identical, FileCompare.AreEqual("FileOne.txt", "FileTwo.txt", b1, b2));
			Assert.AreEqual(FileCompare.Result.Different, FileCompare.AreEqual("FileOne.txt", assemblyFile, b1, b2));
			Assert.AreEqual(FileCompare.Result.File2Missing, FileCompare.AreEqual("FileOne.txt", "xxx", b1, b2));
			Assert.AreEqual(FileCompare.Result.File1Missing, FileCompare.AreEqual("xxx", "FileOne.txt", b1, b2));
			Assert.AreEqual(FileCompare.Result.BothFilesMissing, FileCompare.AreEqual("xxx", "yyy", b1, b2));
			Assert.AreEqual(FileCompare.Result.BothFilesMissing, FileCompare.AreEqual("xxx", "xxx", b1, b2));
			Assert.AreEqual(FileCompare.Result.File1Missing, FileCompare.AreEqual(null, "FileOne.txt", b1, b2));
		}
	}
}


<|start_filename|>tests/SymmetricMatrixTests.cs<|end_filename|>
﻿using System;
using Lidgren.Core;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace UnitTests
{
	[TestClass]
	public class SymmetricMatrixTests
	{
		[TestMethod]
		public void TestSymmetricMatrix()
		{
			TestMatrix(1);
			TestMatrix(2);
			TestMatrix(3);
			TestMatrix(4);
			TestMatrix(5);
			TestMatrix(129);

			TestBoolMatrix(1);
			TestBoolMatrix(2);
			TestBoolMatrix(17);
			TestBoolMatrix(64);
			TestBoolMatrix(65);
			TestBoolMatrix(1023);
			TestBoolMatrix(1024);
			TestBoolMatrix(1025);
		}

		private void TestMatrix(int size)
		{
			var mat = new SymmetricMatrix<int>(size);
			PRNG.Fill<int>(mat.Data);
			for (int x = 0; x < size; x++)
				for (int y = 0; y < size; y++)
					Assert.AreEqual(mat[x, y], mat[y, x]);
		}

		private void TestBoolMatrix(int size)
		{
			var mat = new SymmetricMatrixBool(size);

			// fill
			for (int x = 0; x < size; x++)
				for (int y = 0; y < size; y++)
					mat[x, y] = PRNG.NextBool();

			// check
			for (int x = 0; x < size; x++)
				for (int y = 0; y < size; y++)
					Assert.AreEqual(mat[x, y], mat[y, x]);
		}
	}
}


<|start_filename|>src/PRNG.cs<|end_filename|>
﻿#nullable enable
//#define SPLITMIX64
using System;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

namespace Lidgren.Core
{
	/// <summary>
	/// Random numbers generated by XorShift64*
	/// </summary>
	public static class PRNG
	{
		private const ulong c_star = 0x2545F4914F6CDD1Dul; // 2685821657736338717

		/// <summary>
		/// This is not thread local data by design because that would be slower in the general case
		/// Whenever concurrency might occur; use the ref state overloads with a local variable instead
		/// </summary>
		private static ulong s_state = RandomSeed.GetUInt64();

		/// <summary>
		/// Returns random value between 0 and UInt64.MaxValue using XorShift64*
		/// </summary>
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static ulong NextUInt64(ref ulong state)
		{
			unchecked
			{
#if SPLITMIX64
				state += 0x9e3779b97f4a7c15ul;
				ulong z = state;
				z = (z ^ (z >> 30)) * 0xbf58476d1ce4e5b9;
				z = (z ^ (z >> 27)) * 0x94d049bb133111eb;
				return z ^ (z >> 31);
#else
				var x = state;
				x ^= x >> 12;
				x ^= x << 25;
				x ^= x >> 27;
				state = x;
				return x * c_star;
#endif
			}
		}


		/// <summary>
		/// Returns random value between 0 and UInt64.MaxValue using XorShift64*
		/// </summary>
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static ulong NextUInt64()
		{
			return NextUInt64(ref s_state);
		}

		/// <summary>
		/// Returns random value between 0 and UInt32.MaxValue
		/// </summary>
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static uint NextUInt32(ref ulong state)
		{
			var value = NextUInt64(ref state);
			return (uint)(value >> 32); // use upper bits
		}

		/// <summary>
		/// Returns random value between 0 and UInt32.MaxValue
		/// </summary>
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static uint NextUInt32()
		{
			var value = NextUInt64(ref s_state);
			return (uint)(value >> 32); // use upper bits
		}

		/// <summary>
		/// Returns true or false randomly
		/// </summary>
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static bool NextBool(ref ulong state)
		{
			return BoolUIntUnion.ReinterpretCast(NextUInt32(ref state) >> 16);
		}

		/// <summary>
		/// Returns true or false randomly
		/// </summary>
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static bool NextBool()
		{
			return NextBool(ref s_state);
		}

		/// <summary>
		/// Fills buffer slice with random numbers
		/// </summary>
		public static void NextBytes(byte[] buffer, int offset, int length)
		{
			NextBytes(ref s_state, new Span<byte>(buffer, offset, length));
		}

		public static void NextBytes(byte[] buffer)
		{
			NextBytes(buffer, 0, buffer.Length);
		}

		public static void NextBytes(Span<byte> buffer)
		{
			NextBytes(ref s_state, buffer);
		}

		/// <summary>
		/// Fill span with random bytes
		/// </summary>
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static void NextBytes(ref ulong state, Span<byte> bytes)
		{
			unchecked
			{
				Span<ulong> ulongs = MemoryMarshal.Cast<byte, ulong>(bytes);

				var full = bytes.Length >> 3;
				for (int i = 0; i < full; i++)
				{
					var x = state;
					x ^= x >> 12;
					x ^= x << 25;
					x ^= x >> 27;
					state = x;
					ulongs[i] = x * c_star;
				}

				int offset = full << 3;
				switch (bytes.Length - offset)
				{
					case 0:
					default:
						return;

					case 1:
						bytes[offset] = (byte)NextUInt64(ref state);
						return;

					case 2:
						var r = NextUInt64(ref state);
						bytes[offset] = (byte)r;
						bytes[offset + 1] = (byte)(r >> 8);
						return;

					case 3:
						var r2 = NextUInt64(ref state);
						bytes[offset] = (byte)r2;
						bytes[offset + 1] = (byte)(r2 >> 8);
						bytes[offset + 2] = (byte)(r2 >> 16);
						return;
				}
			}
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static int Next(ref ulong state, int minValue, int maxValue)
		{
			CoreException.Assert(maxValue > minValue);
			unchecked
			{
				var x = state;
				x ^= x >> 12;
				x ^= x << 25;
				x ^= x >> 27;
				state = x;

				// lemire fastrange
				ulong rnd = (x * c_star) >> 32; // use upper bits
				ulong span = (ulong)((long)maxValue - (long)minValue);
				uint offset = (uint)((rnd * span) >> 32);
				var retval = (int)(minValue + offset);

				CoreException.Assert(retval >= minValue && retval < maxValue);

				return retval;
			}
		}

		/// <summary>
		/// Returns a random value larger or equal to minValue and less than maxValue
		/// </summary>
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static int Next(int minValue, int maxValue)
		{
			return Next(ref s_state, minValue, maxValue);
		}

		/// <summary>
		/// Returns random value larger or equal to 0.0 and less than 1.0
		/// </summary>
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static double NextDouble(ref ulong state)
		{
			// generate 52 random mantissa bits and an unbiased exponent of 0
			ulong dval = (NextUInt64(ref state) & 0xFFFFFFFFFFFFFUL) | 0x3FF0000000000000UL;
			double zeroToOne = DoubleULongUnion.ReinterpretCast(dval) - 1.0;
			CoreException.Assert(zeroToOne >= 0.0f && zeroToOne < 1.0);
			return zeroToOne;
		}

		/// <summary>
		/// Returns random value larger or equal to 0.0 and less than 1.0
		/// </summary>
		public static double NextDouble()
		{
			return NextDouble(ref s_state);
		}

		/// <summary>
		/// Returns random value larger or equal to minValue and less than maxValue
		/// </summary>
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static double NextDouble(ref ulong state, double minValue, double maxValue)
		{
			CoreException.Assert(maxValue > minValue);

			var dval = (NextUInt64(ref state) & 0xFFFFFFFFFFFFFUL) | 0x3FF0000000000000UL;
			double zeroToOne = DoubleULongUnion.ReinterpretCast(dval) - 1.0;
			double retval = minValue + ((maxValue - minValue) * zeroToOne);
			CoreException.Assert(retval >= minValue && retval < maxValue);
			return retval;
		}

		/// <summary>
		/// Returns random value larger or equal to minValue and less than maxValue
		/// </summary>
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static double NextDouble(double minValue, double maxValue)
		{
			return NextDouble(ref s_state, minValue, maxValue);
		}

		/// <summary>
		/// Returns random value larger or equal to 0.0f and less than 1.0f
		/// </summary>
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static float NextFloat(ref ulong state)
		{
			uint v = (NextUInt32(ref state) & 0b01111111_11111111_11111111u) | 0x3F800000u;
			float zeroToOne = SingleUIntUnion.ReinterpretCast(v) - 1.0f;
			CoreException.Assert(zeroToOne >= 0.0f && zeroToOne < 1.0);
			return zeroToOne;
		}

		/// <summary>
		/// Returns random value larger or equal to 0.0f and less than 1.0f
		/// </summary>
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static float NextFloat()
		{
			return NextFloat(ref s_state);
		}

		/// <summary>
		/// Returns random value larger or equal to minValue and less than maxValue
		/// </summary>
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static float NextFloat(ref ulong state, float minValue, float maxValue)
		{
			return (float)NextDouble(ref state, minValue, maxValue);
		}

		/// <summary>
		/// Returns random value larger or equal to minValue and less than maxValue
		/// </summary>
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static float NextFloat(float minValue, float maxValue)
		{
			return (float)NextDouble(ref s_state, minValue, maxValue);
		}

		/// <summary>
		/// Returns random point on circle, unit length
		/// </summary>
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static void NextUnitCircle(out float x, out float y)
		{
			NextUnitCircle(ref s_state, out x, out y);
		}

		/// <summary>
		/// Returns random point on circle, unit length
		/// </summary>
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static void NextUnitCircle(ref ulong state, out float x, out float y)
		{
			var azimuth = NextFloat(0, MathF.PI * 2.0f);
			x = MathF.Cos(azimuth);
			y = MathF.Sin(azimuth);
		}

		/// <summary>
		/// Returns random point on sphere, unit length
		/// </summary>
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static void NextUnitSphere(out float x, out float y, out float z)
		{
			NextUnitSphere(ref s_state, out x, out y, out z);
		}

		/// <summary>
		/// Returns random point on sphere, unit length
		/// </summary>
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static Vector3 NextUnitSphere()
		{
			Vector3 retval;
			NextUnitSphere(ref s_state, out retval.X, out retval.Y, out retval.Z);
			return retval;
		}

		/// <summary>
		/// Returns random point on sphere, unit length
		/// </summary>
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static void NextUnitSphere(ref ulong state, out float x, out float y, out float z)
		{
			float dz = NextFloat(ref state) * 2.0f - 1.0f;
			float m = MathF.Sqrt(1.0f - dz * dz);
			float azimuth = ((NextFloat(ref state) * 2.0f - 1.0f) * 0.5f + 0.5f) * 2.0f * MathF.PI;
			x = MathF.Cos(azimuth) * m;
			y = MathF.Sin(azimuth) * m;
			z = dz;
		}

		/// <summary>
		/// Returns random point on sphere, unit length
		/// </summary>
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static void NextUnitSphereDoublePrecision(ref ulong state, out double x, out double y, out double z)
		{
			double dz = NextDouble(ref state) * 2.0 - 1.0;
			double m = Math.Sqrt(1.0 - dz * dz);
			double azimuth = ((NextDouble(ref state) * 2.0 - 1.0) * 0.5 + 0.5) * 2.0 * Math.PI;
			x = Math.Cos(azimuth) * m;
			y = Math.Sin(azimuth) * m;
			z = dz;
		}

		/// <summary>
		/// Returns a random rotation around an axis
		/// </summary>
		public static Quaternion NextAxisRotation(in Vector3 axis)
		{
			return Quaternion.CreateFromAxisAngle(axis, PRNG.NextFloat(0, MathF.PI * 2.0f));
		}

		/// <summary>
		/// Returns a random rotation around a random axis
		/// </summary>
		public static Quaternion NextRotation()
		{
			Vector3 axis;
			NextUnitSphere(out axis.X, out axis.Y, out axis.Z);
			return Quaternion.CreateFromAxisAngle(axis, PRNG.NextFloat(0, MathF.PI * 2.0f));
		}

		/// <summary>
		/// Returns values from a rough bell shape skewed to have its peak at 'mode'  (min <= x < max)
		/// </summary>
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static float NextPseudoGaussian(float min, float max, float mode)
		{
			return NextPseudoGaussian(ref s_state, min, max, mode);
		}

		/// <summary>
		/// Returns values from a rough bell shape skewed to have its peak at 'mode'  (min <= x < max)
		/// </summary>
		public static float NextPseudoGaussian(ref ulong state, float min, float max, float mode)
		{
			float nrmValue;
			nrmValue = (NextFloat(ref state) + NextFloat(ref state) + NextFloat(ref state)) * 0.666666666f;
			if (nrmValue > 1.0f)
				nrmValue = 1.0f - (nrmValue - 1.0f);

			var modep = (mode - min) / (max - min);
			if (NextDouble() < modep)
			{
				// lower half
				var span = mode - min;
				return min + (span * nrmValue);
			}
			else
			{
				// upper half
				var span = max - mode;
				return mode + (span * (1.0f - nrmValue));
			}
		}

		/// <summary>
		/// Returns values from a rough bell shape skewed to have its peak at 'mode'  (min <= x < max)
		/// </summary>
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static double NextPseudoGaussian(double min, double max, double mode)
		{
			return NextPseudoGaussian(ref s_state, min, max, mode);
		}

		/// <summary>
		/// Returns values from a rough bell shape skewed to have its peak at 'mode'  (min <= x < max)
		/// </summary>
		public static double NextPseudoGaussian(ref ulong state, double min, double max, double mode)
		{
			double nrmValue;
			nrmValue = (NextDouble(ref state) + NextDouble(ref state) + NextDouble(ref state)) * 0.666666666f;
			if (nrmValue > 1.0)
				nrmValue = 1.0 - (nrmValue - 1.0);

			var modep = (mode - min) / (max - min);
			if (NextDouble() < modep)
			{
				// lower half
				var span = mode - min;
				return min + (span * nrmValue);
			}
			else
			{
				// upper half
				var span = max - mode;
				return mode + (span * (1.0 - nrmValue));
			}
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static void Shuffle<T>(Span<T> list)
		{
			Shuffle(list, ref s_state);
		}

		/// <summary>
		/// Randomly shuffle the elements (Fisher-Yates)
		/// </summary>
		public static void Shuffle<T>(in Span<T> list, ref ulong state)
		{
			for (int i = 0; i < list.Length - 1; i++)
			{
				int j = Next(i, list.Length);
				T tmp = list[i];
				list[i] = list[j];
				list[j] = tmp;
			}
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static void Fill<T>(Span<T> span) where T : struct
		{
			var bytes = MemoryMarshal.AsBytes<T>(span);
			NextBytes(bytes);
		}
	}
}


<|start_filename|>src/Unions.cs<|end_filename|>
﻿using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

namespace Lidgren.Core
{
	[StructLayout(LayoutKind.Explicit)]
	public struct BoolUIntUnion
	{
		/// <summary>
		/// Value as a boolean
		/// </summary>
		[FieldOffset(0)]
		public bool BoolValue;

		/// <summary>
		/// Value as an unsigned 32 bit integer
		/// </summary>
		[FieldOffset(0)]
		public uint UIntValue;

		public BoolUIntUnion(bool value)
		{
#if NET5_0_OR_GREATER
			Unsafe.SkipInit(out this);
#else
			UIntValue = 0;
#endif
			BoolValue = value;
		}

		public BoolUIntUnion(uint value)
		{
#if NET5_0_OR_GREATER
			Unsafe.SkipInit(out this);
#else
			BoolValue = false;
#endif
			UIntValue = value & 1u; // don't trust the top bits
		}

		public static uint ReinterpretCast(bool value)
		{
			return (new BoolUIntUnion(value)).UIntValue;
		}

		public static bool ReinterpretCast(uint value)
		{
			return (new BoolUIntUnion(value)).BoolValue;
		}
	}

	/// <summary>
	/// Utility struct for writing Singles
	/// </summary>
	[StructLayout(LayoutKind.Explicit)]
	public struct SingleUIntUnion
	{
		/// <summary>
		/// Value as a 32 bit float
		/// </summary>
		[FieldOffset(0)]
		public float SingleValue;

		/// <summary>
		/// Value as an unsigned 32 bit integer
		/// </summary>
		[FieldOffset(0)]
		public uint UIntValue;

		private SingleUIntUnion(float value)
		{
#if NET5_0_OR_GREATER
			Unsafe.SkipInit(out this);
#else
			UIntValue = 0;
#endif
			SingleValue = value;
		}

		private SingleUIntUnion(uint value)
		{
#if NET5_0_OR_GREATER
			Unsafe.SkipInit(out this);
#else
			SingleValue = 0;
#endif
			UIntValue = value;
		}

		public static uint ReinterpretCast(float value)
		{
			return (new SingleUIntUnion(value)).UIntValue;
		}

		public static float ReinterpretCast(uint value)
		{
			return (new SingleUIntUnion(value)).SingleValue;
		}
	}

	/// <summary>
	/// Utility struct for writing Singles
	/// </summary>
	[StructLayout(LayoutKind.Explicit)]
	public ref struct SinglesULongUnion
	{
		[FieldOffset(0)]
		public float SingleValue0;
		[FieldOffset(4)]
		public float SingleValue1;

		/// <summary>
		/// Value as an unsigned 64 bit integer
		/// </summary>
		[FieldOffset(0)]
		public ulong ULongValue;
	}

	/// <summary>
	/// Utility struct for writing Singles
	/// </summary>
	[StructLayout(LayoutKind.Explicit, Pack = 1)]
	public ref struct SingleBytesUnion
	{
		/// <summary>
		/// Value as a 32 bit float
		/// </summary>
		[FieldOffset(0)]
		public float SingleValue;

		[FieldOffset(0)]
		public byte B0;
		[FieldOffset(1)]
		public byte B1;
		[FieldOffset(2)]
		public byte B2;
		[FieldOffset(3)]
		public byte B3;
	}

	[StructLayout(LayoutKind.Explicit)]
	public struct DoubleULongUnion
	{
		/// <summary>
		/// Value as a 64 bit double
		/// </summary>
		[FieldOffset(0)]
		public double DoubleValue;

		/// <summary>
		/// Value as an unsigned 64 bit integer
		/// </summary>
		[FieldOffset(0)]
		public ulong UlongValue;

		public DoubleULongUnion(double value)
		{
#if NET5_0_OR_GREATER
			Unsafe.SkipInit(out this);
#else
			UlongValue = 0;
#endif
			DoubleValue = value;
		}

		public DoubleULongUnion(ulong value)
		{
#if NET5_0_OR_GREATER
			Unsafe.SkipInit(out this);
#else
			DoubleValue = 0;
#endif
			UlongValue = value;
		}

		public static ulong ReinterpretCast(double value)
		{
			return (new DoubleULongUnion(value)).UlongValue;
		}

		public static double ReinterpretCast(ulong value)
		{
			return (new DoubleULongUnion(value)).DoubleValue;
		}
	}

	[StructLayout(LayoutKind.Explicit)]
	public struct TwoSinglesULongUnion
	{
		/// <summary>
		/// Value as a 64 bit double
		/// </summary>
		[FieldOffset(0)]
		public ulong UlongValue;

		[FieldOffset(0)]
		public float F0;

		[FieldOffset(4)]
		public float F1;

		public TwoSinglesULongUnion(float a, float b)
		{
#if NET5_0_OR_GREATER
			Unsafe.SkipInit(out this);
#else
			UlongValue = 0;
#endif
			F0 = a;
			F1 = b;
		}

		public TwoSinglesULongUnion(ulong value)
		{
#if NET5_0_OR_GREATER
			Unsafe.SkipInit(out this);
#else
			F0 = 0;
			F1 = 0;
#endif
			UlongValue = value;
		}

		public static ulong ReinterpretCast(float a, float b)
		{
			return (new TwoSinglesULongUnion(a, b)).UlongValue;
		}

		public static (float, float) ReinterpretCast(ulong value)
		{
			var val = new TwoSinglesULongUnion(value);
			return (val.F0, val.F1);
		}
	}
}


<|start_filename|>tests/UlidTests.cs<|end_filename|>
﻿using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Lidgren.Core;

namespace UnitTests
{
	[TestClass]
	public class UlidTests
	{
		[TestMethod]
		public void TestUlid()
		{
			// just create
			var anything = Ulid.Create();
			Assert.IsTrue(anything.ToString() != null);

			// from timestamp
			var dto = DateTimeOffset.FromUnixTimeMilliseconds(1484581420);
			var ulid = Ulid.Create(dto);
			Assert.IsTrue(ulid.ToString().StartsWith("0001C7STHC"));

			// string roundtrip
			var str = anything.ToString();
			var rev = Ulid.Parse(str);
			Assert.IsTrue(str == rev.ToString());

			// bytes roundtrip
			var bytes = new byte[16];
			anything.AsBytes(bytes);
			var fromBytes = new Ulid(bytes);
			Assert.IsTrue(anything.Equals(fromBytes));
			Assert.IsTrue(anything.ToString() == fromBytes.ToString());

			// test operators
			var one = new Ulid(100UL, 200UL);
			var two = new Ulid(42UL, 43UL);
			var three = new Ulid(42UL, 43UL);
			Assert.IsTrue(one != two);
			Assert.IsTrue(two == three);
		}
	}
}


<|start_filename|>src/DataReader.cs<|end_filename|>
﻿#nullable enable
using System;
using System.IO;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

namespace Lidgren.Core
{
	/// <summary>
	/// Helper class to read incrementally from a Stream
	/// </summary>
	public ref struct DataReader
	{
		private Stream? m_stream;

		private Span<byte> m_buffer;
		private ReadOnlySpan<byte> m_remaining;

		public DataReader(Stream stream, Span<byte> buffer)
		{
			m_stream = stream;
			m_buffer = buffer;
			m_remaining = default;
		}

		public DataReader(ReadOnlySpan<byte> data)
		{
			m_stream = null;
			m_buffer = null;
			m_remaining = data;
		}

		[MethodImpl(MethodImplOptions.NoInlining)]
		private ReadOnlySpan<byte> Fill()
		{
			if (m_stream == null)
				return m_remaining; // nothing to fill from; created from finite data size

			int remLen = m_remaining.Length;
			m_remaining.CopyTo(m_buffer);

			int bytesRead = m_stream.Read(m_buffer.Slice(remLen));
			if (bytesRead == 0)
				throw new Exception("No more bytes to read");

			m_remaining = m_buffer.Slice(0, remLen + bytesRead);
			return m_remaining;
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public bool ReadBool()
		{
			if (m_remaining.Length < 1)
				Fill();
			return m_remaining.ReadBool();
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public ushort ReadUInt16()
		{
			if (m_remaining.Length < 2)
				Fill();
			return m_remaining.ReadUInt16();
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public uint ReadUInt32()
		{
			if (m_remaining.Length < 4)
				Fill();
			return m_remaining.ReadUInt32();
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public ulong ReadUInt64()
		{
			if (m_remaining.Length < 8)
				Fill();
			return m_remaining.ReadUInt64();
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public short ReadInt16()
		{
			if (m_remaining.Length < 2)
				Fill();
			return m_remaining.ReadInt16();
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public int ReadInt32()
		{
			if (m_remaining.Length < 4)
				Fill();
			return m_remaining.ReadInt32();
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public long ReadInt64()
		{
			if (m_remaining.Length < 8)
				Fill();
			return m_remaining.ReadInt64();
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public float ReadSingle()
		{
			if (m_remaining.Length < 4)
				Fill();
			return m_remaining.ReadSingle();
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public double ReadDouble()
		{
			if (m_remaining.Length < 8)
				Fill();
			return m_remaining.ReadDouble();
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public string ReadString()
		{
			try
			{
				return m_remaining.ReadString();
			}
			catch
			{
				Fill();
				return m_remaining.ReadString();
			}
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public int ReadString(Span<char> into)
		{
			try
			{
				return m_remaining.ReadString(into);
			}
			catch
			{
				Fill();
				return m_remaining.ReadString(into);
			}
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void ReadBytes(Span<byte> into)
		{
			if (m_remaining.Length < into.Length)
				Fill();
			m_remaining.ReadBytes(into);
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public ReadOnlySpan<byte> ReadBytes(int count)
		{
			if (m_remaining.Length < count)
				Fill();
			return m_remaining.ReadBytes(count);
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public uint ReadVariableUInt32()
		{
			if (m_remaining.Length < 5)
				Fill();
			return (uint)m_remaining.ReadVariableUInt64();
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public ulong ReadVariableUInt64()
		{
			if (m_remaining.Length < 9)
				Fill();
			return m_remaining.ReadVariableUInt64();
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public long ReadVariableInt64()
		{
			if (m_remaining.Length < 9)
				Fill();
			return m_remaining.ReadVariableInt64();
		}

		/// <summary>
		/// Reads an unmanaged struct (ie containing no reference types)
		/// </summary>
		public T Read<T>() where T : unmanaged
		{
			int size = Unsafe.SizeOf<T>();
			var rem = m_remaining;
			if (rem.Length < size)
				rem = Fill();
			var retval = MemoryMarshal.Read<T>(rem);
			m_remaining = rem.Slice(size);
			return retval;
		}

		public TArrItem[] ReadLengthPrefixedArray<TArrItem>() where TArrItem : unmanaged
		{
			var itemsCount = (int)ReadVariableUInt32();
			var itemSize = Unsafe.SizeOf<TArrItem>();
			var numBytes = itemsCount * itemSize;

			var rem = m_remaining;
			if (rem.Length < numBytes)
				rem = Fill();

			var src = MemoryMarshal.Cast<byte, TArrItem>(rem.Slice(0, numBytes));
			var retval = new TArrItem[itemsCount];
			src.CopyTo(retval);

			rem = rem.Slice(numBytes);
			m_remaining = rem;

			return retval;
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public Vector2 ReadVector2()
		{
			if (m_remaining.Length < 8)
				Fill();
			return new Vector2(
				m_remaining.ReadSingle(),
				m_remaining.ReadSingle()
			);
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public Vector3 ReadVector3()
		{
			if (m_remaining.Length < 12)
				Fill();
			return new Vector3(
				m_remaining.ReadSingle(),
				m_remaining.ReadSingle(),
				m_remaining.ReadSingle()
			);
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public Vector4 ReadVector4()
		{
			if (m_remaining.Length < 16)
				Fill();
			return new Vector4(
				m_remaining.ReadSingle(),
				m_remaining.ReadSingle(),
				m_remaining.ReadSingle(),
				m_remaining.ReadSingle()
			);
		}
	}
}


<|start_filename|>src/Id32.cs<|end_filename|>
﻿#nullable enable
using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;

namespace Lidgren.Core
{
	/// <summary>
	/// 32 bit sequential id. Use Id32.Next() to generate. Valid ids starts at 1.
	/// </summary>
	[DebuggerDisplay("{m_id}")]
	[JsonConverter(typeof(Id32JsonConverter))]
	public readonly struct Id32 : IEquatable<Id32>, IComparable<Id32>, IComparable<int>, IEquatable<int>
	{
		public static readonly Id32 Invalid = new Id32(0);

		private static int s_lastId = 0;

		private readonly int m_id;

		public Id32(int value)
		{
			m_id = value;
		}

		public static Id32 PeekNext()
		{
			return new Id32(s_lastId + 1);
		}

		public static Id32 Next()
		{
			return new Id32(Interlocked.Increment(ref s_lastId));
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static explicit operator int(Id32 id)
		{
			return id.m_id;
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static explicit operator Id32(int val)
		{
			return new Id32(val);
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public readonly int CompareTo(Id32 other)
		{
			if (m_id < other.m_id)
				return -1;
			if (m_id > other.m_id)
				return 1;
			return 0;
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public readonly int CompareTo(int other)
		{
			if (m_id < other)
				return -1;
			if (m_id > other)
				return 1;
			return 0;
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public override bool Equals(object? other)
		{
			return ((other is null) == false) && m_id == ((Id32)other).m_id;
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public bool Equals(Id32 other)
		{
			return m_id == other.m_id;
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static bool operator ==(Id32 x, Id32 y)
		{
			return x.m_id == y.m_id;
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static bool operator >=(Id32 x, Id32 y)
		{
			return x.m_id >= y.m_id;
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static bool operator <=(Id32 x, Id32 y)
		{
			return x.m_id <= y.m_id;
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static bool operator !=(Id32 x, Id32 y)
		{
			return x.m_id != y.m_id;
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static bool operator <(Id32 x, Id32 y)
		{
			return x.m_id < y.m_id;
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static bool operator >(Id32 x, Id32 y)
		{
			return x.m_id > y.m_id;
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public override int GetHashCode()
		{
			return m_id;
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public override string ToString()
		{
			return m_id.ToString();
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public bool Equals(int other)
		{
			return m_id == other;
		}
	}

	public class Id32JsonConverter : JsonConverter<Id32>
	{
		public static readonly Id32JsonConverter Instance = new Id32JsonConverter();

		public override Id32 Read(ref Utf8JsonReader rdr, Type typeToConvert, JsonSerializerOptions options) => (Id32)rdr.GetInt32();

		public override void Write(Utf8JsonWriter wrt, Id32 value, JsonSerializerOptions options)
		{
			wrt.WriteNumberValue((int)value);
		}
	}
}


<|start_filename|>src/Text/StringUtils.cs<|end_filename|>
﻿#nullable enable
using System;
using System.Runtime.CompilerServices;

namespace Lidgren.Core
{
	public static class StringUtils
	{
		private static char[] s_valueToHex = new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };

		/// <summary>
		/// Single hex character to integer; ie. 'B' to 11; supports any casing
		/// </summary>
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static int HexCharToInteger(char c)
		{
			var d = (int)c;
			return (d & 0xf) + (d >> 6) + ((d >> 6) << 3);
		}

		/// <summary>
		/// Convert string of hex characters so span of bytes; ie. "0x011F" to { 1, 31 } - returns number of bytes read
		/// </summary>
		public static int FromHexArray(ReadOnlySpan<char> hexString, Span<byte> into)
		{
			if (hexString.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
				hexString = hexString.Slice(2);
			CoreException.Assert(into.Length >= hexString.Length / 2);
			int len = 0;
			for (int i = 0; i < hexString.Length; i += 2)
			{
				var value = HexCharToInteger(hexString[i]) << 4 | (HexCharToInteger(hexString[i + 1]));
				into[len] = (byte)value;
				len++;
			}
			return len;
		}

		/// <summary>
		/// Convert string of hex characters so array of bytes; ie. "0x011F" to { 1, 31 }
		/// </summary>
		public static byte[] FromHexArray(ReadOnlySpan<char> hexString)
		{
			if (hexString.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
				hexString = hexString.Slice(2);
			var bytes = new byte[hexString.Length / 2];
			int len = FromHexArray(hexString, bytes);
			CoreException.Assert(len == bytes.Length);
			return bytes;
		}

		/// <summary>
		/// Convert string of hex characters to integer; ie. "0x1F" or just "1F" to 31; supports any casing
		/// </summary>
		public static ulong FromHex(ReadOnlySpan<char> hexString)
		{
			if (hexString.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
				hexString = hexString.Slice(2);
			ulong result = 0;
			while (hexString.Length > 0)
			{
				var val = (uint)HexCharToInteger(hexString[0]);
				result = (result << 4) | val;
				hexString = hexString.Slice(1);
			}
			return result;
		}

		/// <summary>
		/// Convert string of bytes representing hex characters in UTF8 format to integer; ie. "0x1F" or just "1F" to 31; supports any casing
		/// </summary>
		public static ulong FromUTF8Hex(ReadOnlySpan<byte> hexUTF8Bytes)
		{
			if (hexUTF8Bytes.Length >= 2)
			{
				if (hexUTF8Bytes[0] == (byte)'0' && hexUTF8Bytes[1] == (byte)'x')
					hexUTF8Bytes = hexUTF8Bytes.Slice(2);
			}
			ulong result = 0;
			while (hexUTF8Bytes.Length > 0)
			{
				uint d = (uint)hexUTF8Bytes[0];
				var val = (d & 0xf) + (d >> 6) + ((d >> 6) << 3);
				result = (result << 4) | val;
				hexUTF8Bytes = hexUTF8Bytes.Slice(1);
			}
			return result;
		}

		/// <summary>
		/// Convert integer to string of hex characters; ie. 31 to "1F"
		/// </summary>
		public static int ToHex(ulong value, Span<char> destination, int zeroPadToCharLength = 0)
		{
			bool ok = value.TryFormat(destination, out int written, format: "X");
			if (!ok)
				return 0; // failed

			int pad = zeroPadToCharLength - written;
			if (pad > 0)
			{
				destination.Slice(0, written).CopyTo(destination.Slice(pad));
				destination.Slice(0, pad).Fill('0');
				return zeroPadToCharLength;
			}
			return written;
		}

		/// <summary>
		/// Convert integer to string of hex characters; ie. 31 to "1F"
		/// </summary>
		public static string ToHex(ulong value, int zeroPadToCharLength = 0)
		{
			Span<char> tmp = stackalloc char[Math.Max(16, zeroPadToCharLength)];
			int written = ToHex(value, tmp, zeroPadToCharLength);
			return tmp.Slice(0, written).ToString();
		}

		/// <summary>
		/// Convert array of bytes to string of hex characters; ie. { 31, 00 } to "1F00"
		/// </summary>
		public static void ToHex(ReadOnlySpan<byte> data, Span<char> into)
		{
			CoreException.Assert(into.Length >= data.Length * 2);
			for (int i = 0; i < data.Length; i++)
			{
				into[i * 2] = s_valueToHex[data[i] >> 4];
				into[i * 2 + 1] = s_valueToHex[data[i] & 0b1111];
			}
		}

		/// <summary>
		/// Convert array of bytes to string of hex characters; ie. { 31, 00 } to "1F00"
		/// </summary>
		public static string ToHex(ReadOnlySpan<byte> data)
		{
			if (data.Length < 1024)
			{
				Span<char> stacktmp = stackalloc char[data.Length * 2];
				ToHex(data, stacktmp);
				return stacktmp.ToString();
			}

			var tmp = new char[data.Length * 2];
			ToHex(data, tmp);
			return new string(tmp);
		}

		private const string c_doubleFixedPoint = "0.###################################################################################################################################################################################################################################################################################################################################################";

		/// <summary>
		/// Culture invariant and without scientific notation (0.00000000000123 -> "0.00000000000123", not "1.23E-12")
		/// </summary>
		public static string DoubleToString(double value, IFormatProvider? culture = null)
		{
			if (culture != null)
				return value.ToString(c_doubleFixedPoint, culture);
			return value.ToString(c_doubleFixedPoint, System.Globalization.CultureInfo.InvariantCulture);
		}
	}
}


<|start_filename|>src/Timing/TimingService.cs<|end_filename|>
﻿#nullable enable
using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Threading;

namespace Lidgren.Core
{
	public static class TimingService
	{
		private static readonly FastList<TimingThread> s_threads = new FastList<TimingThread>(16);
		private static readonly FastList<Action<TimingThread, TimingEntry[], int>> s_listeners = new FastList<Action<TimingThread, TimingEntry[], int>>(2);

		public static bool IsEnabled { get; set; }

		/// <summary>
		/// Add a listener that will be called on the thread that is flushing
		/// </summary>
		public static void AddListener(Action<TimingThread, TimingEntry[], int> listener)
		{
			lock (s_listeners)
				s_listeners.Add(listener);
		}

		/// <summary>
		/// Only call this manually when adding external or "virtual" threads such as "GPU thread" for profiling
		/// </summary>
		public static int RegisterThread(TimingThread thread)
		{
			lock (s_threads)
			{
				var retval = s_threads.Count;
				s_threads.Add(thread);
				return retval;
			}
		}

		public static void TriggerFlush(bool shuttingDown)
		{
			if (shuttingDown)
				IsEnabled = false;

			lock (s_threads)
			{
				foreach (var thread in s_threads.ReadOnlySpan)
					thread.TriggerFlush();

				// trigger flush on _this_ thread immediately
				TimingThread.Instance.InternalFlush();
			}

			if (shuttingDown)
			{
				Thread.Sleep(34); // give other threads some time to flush voluntarily

				// forced flush; let's squeeze out the last remaining scopes in the threads
				foreach (var thread in s_threads.ReadOnlySpan)
				{
					try
					{
						thread.InternalFlush(); // ick!
					}
					catch { } // best effort
				}
			}
		}

		// called by TimingThread
		internal static void Flush(TimingThread thread, TimingEntry[] items, int count)
		{
			foreach (var listener in s_listeners.ReadOnlySpan)
			{
				lock (listener)
					listener(thread, items, count);
			}
		}
	}
}


<|start_filename|>src/MathUtils.cs<|end_filename|>
﻿using System;
using System.Runtime.CompilerServices;

namespace Lidgren.Core
{
	public static class MathUtils
	{
		public const float E = 2.718282f;
		public const float Log10E = 0.4342945f;
		public const float Log2E = 1.442695f;
		public const float Pi = 3.141592653589793239f;
		public const float HalfPi = 1.570796326794896619f;
		public const float QuarterPi = 0.785398163397448310f;
		public const float TwoPi = 6.283185307179586f;
		public const float InvTwoPi = (float)(1.0 / 6.283185307179586);
		public const float PiTimesPi = (float)(3.141592653589793239 * 3.141592653589793239);

		public static readonly int[] Primes = {
			3, 7, 11, 17, 23, 29, 37, 47, 59, 71, 89, 107, 131, 163, 197, 239, 293, 353, 431, 521, 631, 761, 919,
			1103, 1327, 1597, 1931, 2333, 2801, 3371, 4049, 4861, 5839, 7013, 8419, 10103, 12143, 14591,
			17519, 21023, 25229, 30293, 36353, 43627, 52361, 62851, 75431, 90523, 108631, 130363, 156437,
			187751, 225307, 270371, 324449, 389357, 467237, 560689, 672827, 807403, 968897, 1162687, 1395263,
			1674319, 2009191, 2411033, 2893249, 3471899, 4166287, 4999559, 5999471, 7199369 };

		/// <summary>
		/// Branchless sign; returns 0 if value < 0 and 1 if value >= 0
		/// </summary>
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static int IsPositive(int v)
		{
			return (int)(1u ^ ((uint)v >> (int)31)); // if v < 0 then 0, else 1
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static int Max(int x, int y)
		{
			// return a > b ? a : b;
			int z = x - y;
			return x - ((z >> 31) & z);
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static float Max(float a, float b)
		{
			return a > b ? a : b;
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static float Max(float a, float b, float c)
		{
			return a > b ? (a > c ? a : c) : (b > c ? b : c);
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static double Max(double a, double b)
		{
			return a > b ? a : b;
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static int Min(int a, int b)
		{
			return a < b ? a : b;
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static uint Min(uint a, uint b)
		{
			return a < b ? a : b;
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static float Min(float a, float b)
		{
			return a < b ? a : b;
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static double Min(double a, double b)
		{
			return a < b ? a : b;
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static bool IsPowerOfTwo(int x)
		{
			return ((x & (x - 1)) == 0);
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static int NextPowerOfTwo(int x)
		{
			x--;
			x |= x >> 1;
			x |= x >> 2;
			x |= x >> 4;
			x |= x >> 8;
			x |= x >> 16;
			x++;
			return x;
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static double Clamp(double value, double min, double max)
		{
			return (value > max) ? max : (value < min) ? min : value;
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static float Clamp(float value, float min, float max)
		{
			return (value > max) ? max : (value < min) ? min : value;
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static int Clamp(int value, int min, int max)
		{
			return (value > max) ? max : (value < min) ? min : value;
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static float SmoothStep(float value1, float value2, float amount)
		{
			float num = MathUtils.Clamp(amount, 0f, 1f);
			return Interpolate(value1, value2, (num * num) * (3f - (2f * num)));
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static double SmoothStep(double value1, double value2, double amount)
		{
			var num = Clamp(amount, 0.0, 1.0);
			return Interpolate(value1, value2, (num * num) * (3f - (2f * num)));
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static float SmoothStep(float amount)
		{
			float num = MathUtils.Clamp(amount, 0f, 1f);
			return (num * num) * (3f - (2f * num));
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static double SmoothStep(double amount)
		{
			var num = Clamp(amount, 0.0, 1.0);
			return (num * num) * (3.0 - (2.0 * num));
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static double SmootherStep(double amount)
		{
			var num = Clamp(amount, 0.0, 1.0);
			return num * num * num * (num * (num * 6.0 - 15.0) + 10.0);
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static float Interpolate(float a, float b, float t)
		{
			return MathF.FusedMultiplyAdd(a, (1 - t), (b * t));
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static byte Interpolate(byte a, byte b, float t)
		{
			return (byte)(a * (1 - t) + b * t);
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static double Interpolate(double a, double b, double t)
		{
			return (a * (1 - t) + b * t);
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static double DegreesToRadians(double degrees)
		{
			//return (Math.PI / 180.0) * degrees;
			return 0.017453292519943295 * degrees;
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static float DegreesToRadians(float degrees)
		{
			//return (float)(Math.PI / 180.0) * degrees;
			return (float)(0.017453292519943295 * degrees);
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static float RadiansToDegrees(float radians)
		{
			return (float)(57.295779513082320876798154814105 * radians);
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static double RadiansToDegrees(double radians)
		{
			return 57.295779513082320876798154814105 * radians;
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static int ToNearestMultiple(int n, int multiple)
		{
			return ((n + multiple - 1) / multiple) * multiple;
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static float AngleToSlope(float degrees)
		{
			return MathF.Tan(MathUtils.DegreesToRadians(degrees));
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static double AngleToSlope(double degrees)
		{
			return Math.Tan(MathUtils.DegreesToRadians(degrees));
		}

		/// <summary>
		/// return x % y but y must be power of two
		/// </summary>
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static int RemainderByPowerOfTwo(int x, int y)
		{
			return x & (y - 1);
		}

		/// <summary>
		/// return x % y but y must be power of two
		/// </summary>
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static long RemainderByPowerOfTwo(long x, long y)
		{
			return x & (y - 1);
		}

		/// <summary>
		/// Euclidean Modulo (not just remainder)
		/// </summary>
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static double Modulo(double value, double mod)
		{
			var m = value % mod;
			if (m < 0)
				m = (mod < 0) ? m - mod : m + mod;
			return m;
		}

		/// <summary>
		/// Euclidean Modulo (not just remainder)
		/// </summary>
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static float Modulo(float value, float mod)
		{
			var m = value % mod;
			if (m < 0)
				m = (mod < 0) ? m - mod : m + mod;
			return m;
		}

		/// <summary>
		/// Euclidean Modulo (not just remainder)
		/// </summary>
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static int Modulo(int value, int mod)
		{
			var m = value % mod;
			if (m < 0)
				m = (mod < 0) ? m - mod : m + mod;
			return m;
		}

		/// <summary>
		/// population standard deviation
		/// </summary>
		public static float StandardDeviation(ReadOnlySpan<float> values)
		{
			double avg = 0.0;
			foreach (var val in values)
				avg += val;
			avg = avg / (double)values.Length;

			double sqrdAvg = 0.0;
			foreach (var val in values)
			{
				var delta = val - avg;
				sqrdAvg += (delta * delta);
			}
			return MathF.Sqrt((float)sqrdAvg / (float)values.Length);
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static float FasterSin(float radians)
		{
			CoreException.Assert(radians >= -MathUtils.Pi && radians <= MathUtils.Pi);
			if (radians < 0)
				return radians * (1.27323954f + 0.405284735f * radians);
			else
				return radians * (1.27323954f - 0.405284735f * radians);
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static float FastSin(float radians)
		{
			//var ranged = x - MathUtils.TwoPi * MathF.Floor((x + MathUtils.Pi) / MathUtils.TwoPi);
			const float negTwoPi = -MathUtils.TwoPi;
			var pre = MathF.Floor((radians + MathUtils.Pi) * MathUtils.InvTwoPi);
			var ranged = MathF.FusedMultiplyAdd(negTwoPi, pre, radians);

			var xa = (MathUtils.Pi - ranged) * ranged;
			var over = 16 * xa;
			var under = MathF.FusedMultiplyAdd(5, MathUtils.PiTimesPi, (xa * -4));
			return over / under;
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static float FastCos(float radians)
		{
			// cos(x) = sin(x + PI/2)
			return FastSin(radians + MathUtils.HalfPi);
		}

		public static bool IsPrime(int candidate)
		{
			if ((candidate & 1) != 0)
			{
				int num = (int)Math.Sqrt((double)candidate);
				for (int i = 3; i <= num; i += 2)
				{
					if (candidate % i == 0)
						return false;
				}
				return true;
			}
			return candidate == 2;
		}

		public static int GetPrime(int min)
		{
			for (int i = 0; i < Primes.Length; i++)
			{
				int num = Primes[i];
				if (num >= min)
					return num;
			}

			for (int j = min | 1; j < int.MaxValue; j += 2)
			{
				if (IsPrime(j) && (j - 1) % 101 != 0)
					return j;
			}
			return min;
		}

		public static int ExpandPrime(int oldSize)
		{
			int num = 2 * oldSize;
			if ((uint)num > 2146435069u && 2146435069 > oldSize)
			{
				return 2146435069;
			}
			return GetPrime(num);
		}

		/// <summary>
		/// Reduce a floating point value; so that its base-10 representation is as few decimals as possible
		/// </summary>
		public static float CropMantissa(float value, int bits)
		{
			SingleUIntUnion un;
			un.UIntValue = 0;
			un.SingleValue = value;

			// reduce by one bit at a time
			uint signAndExponent = un.UIntValue & 0b11111111_10000000_00000000_00000000u;
			uint mantissa = un.UIntValue & 0b00000000_01111111_11111111_11111111u;

			uint mask = ~0u << bits;
			mantissa = mantissa & mask;

			un.UIntValue = signAndExponent | mantissa;
			return un.SingleValue;
		}
	}
}


<|start_filename|>tests/SpanExtensionsTests.cs<|end_filename|>
﻿using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;

using Lidgren.Core;
using System.Numerics;

namespace UnitTests
{
	[TestClass]
	public class SpanExtensionsTests
	{
		[TestMethod]
		public void Test7BitEncoding()
		{
			byte[] arr = new byte[1024];

			// test variable lengths
			var numbers = new ulong[7];
			numbers = new ulong[] { 1, 25, 137, 255, 256, 10000, 1000000 };

			ulong state = RandomSeed.GetUInt64();
			for (int runs = 0; runs < 250000; runs++)
			{
				var work = arr.AsSpan();
				for (int i = 0; i < numbers.Length; i++)
					work.WriteVariableUInt64(numbers[i]);

				int resultLength = arr.Length - work.Length;

				ReadOnlySpan<byte> res = arr.AsSpan(0, resultLength);

				for (int i = 0; i < numbers.Length; i++)
				{
					var nr = res.ReadVariableUInt64();
					Assert.AreEqual(numbers[i], nr);
				}

				// re-randomize
				numbers[0] = (ulong)PRNG.Next(ref state, 0, 6);
				numbers[1] = (ulong)PRNG.Next(ref state, 7, 300);
				numbers[2] = (ulong)PRNG.Next(ref state, 300, 5000);
				numbers[3] = (ulong)PRNG.Next(ref state, 5000, 50000);
				numbers[4] = (ulong)PRNG.Next(ref state, 50000, 500000);
				numbers[5] = (ulong)PRNG.NextUInt64(ref state);
				numbers[6] = (ulong)PRNG.NextUInt64(ref state);
			}

			// signed 64 bit
			var signed64 = new long[7];
			for (int runs = 0; runs < 250000; runs++)
			{
				// re-randomize
				signed64[0] = (long)PRNG.Next(ref state, -5, 5);
				signed64[1] = (long)PRNG.Next(ref state, -100, 100);
				signed64[2] = (long)PRNG.Next(ref state, -300, 300);
				signed64[3] = (long)PRNG.Next(ref state, -5000, 5000);
				signed64[4] = (long)PRNG.Next(ref state, -70000, 70000);
				signed64[5] = (long)PRNG.NextUInt64(ref state);
				signed64[6] = (long)PRNG.NextUInt64(ref state);

				var work = arr.AsSpan();
				for (int i = 0; i < signed64.Length; i++)
					work.WriteVariableInt64(signed64[i]);

				int resultLength = arr.Length - work.Length;

				ReadOnlySpan<byte> res = arr.AsSpan(0, resultLength);

				for (int i = 0; i < signed64.Length; i++)
				{
					var nr = res.ReadVariableInt64();
					Assert.AreEqual(signed64[i], nr);
				}
			}

			// signed int32
			var signed32 = new int[7];
			for (int runs = 0; runs < 250000; runs++)
			{
				// re-randomize
				signed32[0] = (int)PRNG.Next(ref state, -5, 5);
				signed32[1] = (int)PRNG.Next(ref state, -100, 100);
				signed32[2] = (int)PRNG.Next(ref state, -300, 300);
				signed32[3] = (int)PRNG.Next(ref state, -5000, 5000);
				signed32[4] = (int)PRNG.Next(ref state, -70000, 70000);
				signed32[5] = (int)PRNG.NextUInt32(ref state);
				signed32[6] = (int)PRNG.NextUInt32(ref state);

				var work = arr.AsSpan();
				for (int i = 0; i < signed32.Length; i++)
					work.WriteVariableInt32(signed32[i]);

				int resultLength = arr.Length - work.Length;

				ReadOnlySpan<byte> res = arr.AsSpan(0, resultLength);

				for (int i = 0; i < signed64.Length; i++)
				{
					var nr = res.ReadVariableInt32();
					Assert.AreEqual(signed32[i], nr);
				}
			}
		}

		[TestMethod]
		public void TestSpanExtensions()
		{
			var arr = new byte[1024];

			var span = arr.AsSpan();
			span.WriteBool(true);
			span.WriteDouble(1.2);
			span.WriteString("Pararibulitis");
			span.WriteUInt16(12);
			span.Write<Vector2>(new Vector2(1.0f, 2.0f));
			span.WriteLengthPrefixedArray<byte>(new byte[] { 43, 42, 41 });
			span.Write<Vector3>(new Vector3(1.0f, 2.0f, 3.0f));
			span.Write<Vector4>(new Vector4(1.0f, 2.0f, 3.0f, 4.0f));
			span.Write<Quaternion>(new Quaternion(1.0f, 2.0f, 3.0f, 4.0f));
			span.WriteInt32(-120);
			span.WriteString("Pickle Rick");

			var len = arr.Length - span.Length;

			var rdr = new ReadOnlySpan<byte>(arr, 0, len);
			Assert.IsTrue(rdr.ReadBool());
			Assert.AreEqual(1.2, rdr.ReadDouble());
			Assert.AreEqual("Pararibulitis", rdr.ReadString());
			Assert.AreEqual(12, rdr.ReadUInt16());
			Assert.IsTrue(rdr.Read<Vector2>() == new Vector2(1.0f, 2.0f));
			var barr = rdr.ReadLengthPrefixedArray<byte>();
			Assert.AreEqual(3, barr.Length);
			Assert.AreEqual(43, barr[0]);
			Assert.AreEqual(42, barr[1]);
			Assert.AreEqual(41, barr[2]);
			Assert.IsTrue(rdr.Read<Vector3>() == new Vector3(1.0f, 2.0f, 3.0f));
			Assert.IsTrue(rdr.Read<Vector4>() == new Vector4(1.0f, 2.0f, 3.0f, 4.0f));
			Assert.IsTrue(rdr.Read<Quaternion>() == new Quaternion(1.0f, 2.0f, 3.0f, 4.0f));
			Assert.AreEqual(-120, rdr.ReadInt32());

			var tmp = new char[32];
			var plen = rdr.ReadString(tmp.AsSpan());
			Assert.AreEqual("Pickle Rick".Length, plen);
			Assert.IsTrue(tmp.AsSpan(0, plen).SequenceEqual("Pickle Rick".AsSpan()));

			Assert.AreEqual(0, rdr.Length);
		}
	}
}


<|start_filename|>src/Collections/AddList.cs<|end_filename|>
﻿using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;

namespace Lidgren.Core
{
	/// <summary>
	/// Add only list
	/// </summary>
	[DebuggerDisplay("AddList<{typeof(T).Name}> {Count}/{Capacity}")]
	public class AddList<T>
	{
		private T[] m_items = System.Array.Empty<T>();

		private int m_count;
		public int Count => m_count;

		public ReadOnlySpan<T> Span => new Span<T>(m_items, 0, m_count);
		public ReadOnlySpan<T> ReadOnlySpan => new ReadOnlySpan<T>(m_items, 0, m_count);

		public AddList()
		{
		}

		public AddList(int capacity)
		{
			m_items = new T[capacity];
		}

		// cold path, never inline
		[MethodImpl(MethodImplOptions.NoInlining)]
		private T[] Grow(int minAdd)
		{
			var oldLen = m_items.Length;
			var newLength = oldLen == 0 ? 4 : oldLen * 2;
			var min = m_count + minAdd;
			if (newLength < min)
				newLength = min;
			var newBuffer = new T[newLength];

			var old = this.ReadOnlySpan;
			old.CopyTo(newBuffer);

			m_items = newBuffer;
			return newBuffer;
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void Add(in T item)
		{
			ref var loc = ref AddUninitialized();
			loc = item;
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public ref T Add()
		{
			ref var loc = ref AddUninitialized();
			loc = default(T);
			return ref loc;
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public ref T AddUninitialized()
		{
			int count = m_count;
			var buffer = m_items;

			if (count == buffer.Length)
				buffer = Grow(1);

			m_count = count + 1;
			return ref buffer[count];
		}

		public Span<T> AddRange(int numItems)
		{
			int rem = m_items.Length - m_count;
			if (rem < numItems)
				Grow(numItems);
			var span = m_items.AsSpan(m_count, numItems);
			m_count += numItems;
			return span;
		}

		public void AddRange(ReadOnlySpan<T> items)
		{
			var into = AddRange(items.Length);
			items.CopyTo(into);
		}

		public T[] ToArray()
		{
			var span = ReadOnlySpan;
			var retval = new T[span.Length];
			span.CopyTo(retval);
			return retval;
		}
	}
}


<|start_filename|>src/Timing/LockTiming.cs<|end_filename|>
﻿using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Threading;

namespace Lidgren.Core
{
	/// <summary>
	/// Example usage: using (new LockTiming("timerName", myLockObject)) { ... } - will add waitLock scope if lock cannot be taken immediately
	/// </summary>
	public ref struct LockTiming
	{
		public string Name;
		public long Started;
		public long WaitEnd;
		public object LockObject;

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public LockTiming(string name, object lockObject)
		{
			CoreException.Assert(string.IsNullOrWhiteSpace(name) == false);
			Name = name;
			LockObject = lockObject;
			Started = Stopwatch.GetTimestamp();

			var ok = Monitor.TryEnter(lockObject);
			if (!ok)
			{
				Monitor.Enter(lockObject);
				WaitEnd = Stopwatch.GetTimestamp();
			}
			else
			{
				WaitEnd = Started;
			}
		}

		/// <summary>
		/// No need/possibility to implement IDisposable; but compiler magic will call this anyway!
		/// </summary>
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void Dispose()
		{
			Monitor.Exit(LockObject);

			long now = Stopwatch.GetTimestamp();
			CoreException.Assert(now >= Started);
			long duration = now - Started;
			var waited = WaitEnd - Started;
			if (duration > 0 && duration < int.MaxValue)
			{
				TimingThread.Instance.AddEntry(Name, Started, (int)duration);
				if (waited > 0)
					TimingThread.Instance.AddEntry("lockWait", Started, (int)waited);
			}
		}
	}
}


<|start_filename|>src/Collections/FastDictionary.cs<|end_filename|>
﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;

namespace Lidgren.Core
{
	/// <summary>
	/// Dictionary of K,V equivalent with GetOrInit() TryGetRef() and better performance
	/// </summary>
	[DebuggerDisplay("Count = {Count}")]
	public sealed class FastDictionary<TKey, TValue> :
		IReadOnlyCollection<KeyValuePair<TKey, TValue>>,
		IEnumerable<KeyValuePair<TKey, TValue>>
		where TKey : IEquatable<TKey>
	{
		[DebuggerDisplay("Key: {Key} Next: {Next}")]
		private struct Entry
		{
			// If x >= 0:   Holds value; next entry in chain is: x - 1  (result -1 means end of chain)
			//
			// If x < 0:    Freed value or unused; next freed value in chain is: -(x + 2)  (result -1 means unused; end of free chain)
			//
			//  Examples:
			//  -4    Freed value; next freed value in chain is slot 2 ( -(x + 2) )
			//  -3    Freed value; next freed value in chain is slot 1 ( -(x + 2) )
			//  -2    Freed value; next freed value in chain is slot 0 ( -(x + 2) )
			//  -1    Unused entry
			//   0    Holds value; end of chain
			//   1    Holds value; next in chain is slot 0 (x - 1)
			//   2    Holds value; next in chain is slot 1 (x - 1)
			public int Next;

			public TKey Key;
		}

		private int[] m_lookup;
		private Entry[] m_entries;
		private TValue[] m_values;
		private int m_count;
		private int m_freeListIndex;
		private int m_capacityMinusOne;

		public int Count { get { return m_count; } }

		public FastDictionary(int initialCapacity = 64)
		{
			int capacity = MathUtils.NextPowerOfTwo(initialCapacity);

			m_lookup = new int[capacity];
			m_capacityMinusOne = capacity - 1;

			m_entries = new Entry[capacity];
			m_values = new TValue[capacity];

			Clear();
			Validate();
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public bool ContainsKey(TKey key)
		{
			int index = m_lookup[KeyToLookupIndex(key)];
			var entries = m_entries;
			while (index >= 0)
			{
				ref var entry = ref entries[index];
				if (key.Equals(entry.Key))
					return true;
				index = entry.Next - 1;
			}
			return false;
		}

		public bool ContainsValue(in TValue value)
		{
			int count = m_count;
			var entries = m_entries;
			int examined = 0;
			for (int i = 0; i < entries.Length && examined < count; i++)
			{
				ref var item = ref entries[i];
				if (item.Next >= 0)
				{
					examined++;
					if (EqualityComparer<TValue>.Default.Equals(m_values[i], value))
						return true;
				}
			}
			return false;
		}

		private static readonly Entry s_emptyEntry = new Entry() { Next = -1, Key = default(TKey) };

		public void Clear()
		{
			m_lookup.AsSpan().Fill(-1);
			m_entries.AsSpan().Fill(s_emptyEntry);
			m_count = 0;
			m_freeListIndex = -1;

			Validate();
		}

		private static TValue s_dummyValue = default;

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		private int KeyToLookupIndex(TKey key)
		{
			return key.GetHashCode() & m_capacityMinusOne;
		}

		public ref TValue TryGetRef(TKey key, out bool wasFound)
		{
			int index = m_lookup[KeyToLookupIndex(key)];
			var entries = m_entries;
			while (index >= 0)
			{
				ref var entry = ref entries[index];
				if (key.Equals(entry.Key))
				{
					wasFound = true;
					return ref m_values[index];
				}
				index = entry.Next - 1;
			}
			wasFound = false;
			return ref s_dummyValue;
		}

		public TValue this[TKey key]
		{
			[MethodImpl(MethodImplOptions.AggressiveInlining)]
			get
			{
				ref var value = ref TryGetRef(key, out bool found);
				if (found)
					return value;
				CoreException.Throw("Index not found");
				return default(TValue);
			}

			[MethodImpl(MethodImplOptions.AggressiveInlining)]
			set
			{
				ref var val = ref GetOrInit(key, out _);
				val = value;
			}
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void Add(TKey key, in TValue value)
		{
			ref var val = ref GetOrInit(key, out _);
			val = value;
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public bool TryGetValue(TKey key, out TValue value)
		{
			ref var valueByRef = ref TryGetRef(key, out bool found);
			if (found)
			{
				value = valueByRef;
				return true;
			}
			value = default(TValue);
			return false;
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public TValue GetValueOrDefault(TKey key)
		{
			ref var valueByRef = ref TryGetRef(key, out bool found);
			if (found)
				return valueByRef;
			return default(TValue);
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void Set(TKey key, in TValue value)
		{
			ref var val = ref GetOrInit(key, out _);
			val = value;
		}

		[MethodImpl(MethodImplOptions.AggressiveOptimization)]
		public ref TValue GetOrInit(TKey key, out bool wasCreated)
		{
			var entries = m_entries;

			int bucketIndex = KeyToLookupIndex(key);
			int chainLength = 0;

			int entryIndex = m_lookup[bucketIndex];
			if (entryIndex >= 0)
			{
				for (; ; )
				{
					ref Entry entry = ref entries[entryIndex];
					if (key.Equals(entry.Key))
					{
						// found!
						wasCreated = false;
						return ref m_values[entryIndex];
					}
					var next = entry.Next;
					if (next < 1)
						break;
					entryIndex = entry.Next - 1;
					chainLength++;
				}
			}

			// not found; add
			int addedAsIndex;

			if (m_freeListIndex != -1)
			{
				// get slot from free list
				addedAsIndex = m_freeListIndex;
				m_freeListIndex = -(entries[addedAsIndex].Next + 2);
			}
			else
			{
				if (m_count >= entries.Length || chainLength > 100)
				{
					Grow();
					return ref GetOrInit(key, out wasCreated);
				}
				addedAsIndex = m_count;
			}
			m_count++;

			if (entryIndex == -1)
			{
				// first in bucket
				m_lookup[bucketIndex] = addedAsIndex;
			}
			else
			{
				// added to chain
				entries[entryIndex].Next = addedAsIndex + 1;
			}

			ref Entry newEntry = ref entries[addedAsIndex];
			newEntry.Key = key;
			newEntry.Next = 0; // last in chain; no next item

			Validate();

			wasCreated = true;
			return ref m_values[addedAsIndex];
		}

		[MethodImpl(MethodImplOptions.NoInlining)]
		private void Grow()
		{
			int newSize = m_entries.Length << 1;

			CoreException.Assert(m_count == m_entries.Length);
			CoreException.Assert(MathUtils.IsPowerOfTwo(newSize));
			m_capacityMinusOne = newSize - 1;

			m_lookup = new int[newSize];
			m_lookup.AsSpan().Fill(-1);

			Array.Resize(ref m_entries, newSize);
			Array.Resize(ref m_values, newSize);

			// loop over all entries and reinsert them into bucket lists
			var entries = m_entries;
			int count = m_count;
			int mask = m_capacityMinusOne;
			for (int i = 0; i < count; i++)
			{
				ref var entry = ref entries[i];
				var hash = entry.Key.GetHashCode();
				var bucketIndex = hash & mask;

				// place first in chain
				ref var bucket = ref m_lookup[bucketIndex];
				entry.Next = bucket + 1;
				bucket = i;
			}

			m_freeListIndex = m_count;

			for (int i = m_count; i < entries.Length; i++)
			{
				ref var entry = ref entries[i];
				entry.Next = -1;
			}

			Validate();
		}

		[MethodImpl(MethodImplOptions.AggressiveOptimization)]
		public bool Remove(TKey key)
		{
			int bucketIndex = KeyToLookupIndex(key);
			int index = m_lookup[bucketIndex];
			int previousIndex = -1;
			var entries = m_entries;
			var lookup = m_lookup;
			while (index >= 0)
			{
				ref Entry indexEntry = ref entries[index];
				if (key.Equals(indexEntry.Key))
				{
					// found it, bypass
					if (previousIndex >= 0)
						entries[previousIndex].Next = indexEntry.Next;
					else
						lookup[bucketIndex] = indexEntry.Next - 1;

					// add first in free list
					indexEntry.Next = -(m_freeListIndex + 2);
					m_freeListIndex = index;
					m_count--;

					Validate();
					return true;
				}
				previousIndex = index;
				index = indexEntry.Next - 1;
			}

			Validate();
			return false;
		}

		[Conditional("DEBUG")]
		public void Validate()
		{
			var count = 0;
			var lookup = m_lookup;
			var entries = m_entries;
			for (int b = 0; b < lookup.Length; b++)
			{
				int idx = lookup[b];
				while (idx >= 0)
				{
					count++;
					idx = entries[idx].Next - 1;
				}
			}
			CoreException.Assert(count == m_count);
		}

		public void CopyKeys(Span<TKey> target)
		{
			CoreException.Assert(target.Length >= m_count);

			int count = m_count;
			var entries = m_entries;
			int added = 0;
			for (int i = 0; i < entries.Length && added < count; i++)
			{
				ref var item = ref entries[i];
				if (item.Next >= 0)
					target[added++] = item.Key;
			}
		}

		public int CopyValues(Span<TValue> target)
		{
			int count = Math.Min(target.Length, m_count);
			var entries = m_entries;
			var values = m_values;
			int added = 0;
			for (int i = 0; i < entries.Length && added < count; i++)
			{
				ref var item = ref entries[i];
				if (item.Next >= 0)
					target[added++] = values[i];
			}
			return count;
		}

		public Enumerator GetEnumerator() => new Enumerator(this);
		IEnumerator<KeyValuePair<TKey, TValue>> IEnumerable<KeyValuePair<TKey, TValue>>.GetEnumerator() => new Enumerator(this);
		IEnumerator IEnumerable.GetEnumerator() => new Enumerator(this);

		public struct Enumerator : IEnumerator<KeyValuePair<TKey, TValue>>
		{
			private readonly FastDictionary<TKey, TValue> m_dictionary;
			private int m_index;
			private int m_uncounted;
			private KeyValuePair<TKey, TValue> m_current;

			public readonly KeyValuePair<TKey, TValue> Current => m_current;
			object IEnumerator.Current => m_current;

			internal Enumerator(FastDictionary<TKey, TValue> dictionary)
			{
				m_dictionary = dictionary;
				m_index = 0;
				m_uncounted = dictionary.m_count;
				m_current = default;
			}

			/// <summary>
			/// Move to next
			/// </summary>
			public bool MoveNext()
			{
				if (m_uncounted == 0)
				{
					m_current = default;
					return false;
				}

				m_uncounted--;

				var entries = m_dictionary.m_entries;
				var index = m_index;
				for (; ; )
				{
					ref var entry = ref entries[index];
					if (entry.Next >= 0)
					{
						m_current = new KeyValuePair<TKey, TValue>(
							entry.Key,
							m_dictionary.m_values[index]
						);
						m_index = index + 1;
						return true;
					}
					index++;
				}
			}

			void IEnumerator.Reset()
			{
				m_index = 0;
				m_uncounted = m_dictionary.m_count;
				m_current = default;
			}

			public void Dispose() { }
		}

		//
		// Values
		//

		public ValuesEnumerator Values => new ValuesEnumerator(this);

		public struct ValuesEnumerator : IEnumerator<TValue>, IEnumerable<TValue>
		{
			private readonly FastDictionary<TKey, TValue> m_dictionary;
			private int m_index;
			private int m_uncounted;
			private TValue m_current;

			public readonly TValue Current => m_current;
			object IEnumerator.Current => m_current;

			internal ValuesEnumerator(FastDictionary<TKey, TValue> dictionary)
			{
				m_dictionary = dictionary;
				m_index = 0;
				m_uncounted = dictionary.m_count;
				m_current = default;
			}

			/// <summary>
			/// Move to next
			/// </summary>
			public bool MoveNext()
			{
				if (m_uncounted == 0)
				{
					m_current = default;
					return false;
				}

				m_uncounted--;

				var entries = m_dictionary.m_entries;
				var index = m_index;
				for (; ; )
				{
					ref var entry = ref entries[index];
					if (entry.Next >= 0)
					{
						m_current = m_dictionary.m_values[index];
						m_index = index + 1;
						return true;
					}
					index++;
				}
			}

			void IEnumerator.Reset()
			{
				m_index = 0;
				m_uncounted = m_dictionary.m_count;
				m_current = default;
			}

			public void Dispose() { }

			public IEnumerator<TValue> GetEnumerator()
			{
				return this;
			}

			IEnumerator IEnumerable.GetEnumerator()
			{
				return this;
			}
		}

		//
		// Keys
		//
		public KeysEnumerator Keys => new KeysEnumerator(this);

		public struct KeysEnumerator : IEnumerator<TKey>, IEnumerable<TKey>
		{
			private readonly FastDictionary<TKey, TValue> m_dictionary;
			private int m_index;
			private TKey m_current;

			public TKey Current => m_current;
			object IEnumerator.Current => m_current;

			internal KeysEnumerator(FastDictionary<TKey, TValue> dictionary)
			{
				m_dictionary = dictionary;
				m_index = 0;
				m_current = default;
			}

			[MethodImpl(MethodImplOptions.AggressiveOptimization)]
			public bool MoveNext()
			{
				var entries = m_dictionary.m_entries;
				int index = m_index;
				while (index < entries.Length)
				{
					ref Entry entry = ref entries[index++];
					if (entry.Next >= 0)
					{
						m_current = entry.Key;
						m_index = index;
						return true;
					}
				}
				m_current = default;
				return false;
			}

			void IEnumerator.Reset()
			{
				m_index = 0;
				m_current = default;
			}

			public void Dispose() { }

			public IEnumerator<TKey> GetEnumerator()
			{
				return this;
			}

			IEnumerator IEnumerable.GetEnumerator()
			{
				return this;
			}
		}
	}
}


<|start_filename|>tests/DynamicOrderingTests.cs<|end_filename|>
﻿using System;
using System.Diagnostics;
using System.Threading;
using Lidgren.Core;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace UnitTests
{
	public enum ConcType
	{
		Car,
		Color,
		Game,
		Fruit
	}

	[TestClass]
	public class DynamicOrderingTests
	{
		[TestMethod]
		public void TestDynamicOrdering()
		{
			JobService.Initialize();

			var list = new IDynamicOrdered[]
			{
				new Feet(), new Torso(), new Legs(), new Head(), new Neck()
			};

			bool ok = DynamicOrdering.Perform(list.AsSpan(), out var err);

			Assert.IsTrue(ok);
			Assert.AreEqual(typeof(Head), list[0].GetType());
			Assert.AreEqual(typeof(Neck), list[1].GetType());
			Assert.AreEqual(typeof(Torso), list[2].GetType());
			Assert.AreEqual(typeof(Legs), list[3].GetType());
			Assert.AreEqual(typeof(Feet), list[4].GetType());


			var items = new TestItem[]
			{
				new TestItem("Volvo", 4, ConcType.Car, 0),
				new TestItem("Audi", 3, ConcType.Car, 1),
				new TestItem("Mazda", 6, ConcType.Car, 1),
				new TestItem("BMW", 4, ConcType.Car, 2),
				new TestItem("Opel", 1, ConcType.Car, 1),
				new TestItem("Tesla", 1, ConcType.Car, 1),

				new TestItem("Blue", 4, ConcType.Color, 1),
				new TestItem("AliceBlue", 4, ConcType.Color, 1),
				new TestItem("Chartreuse", 2, ConcType.Color, 1),
				new TestItem("Pink", 4, ConcType.Color, 1),
				new TestItem("Orange", 2, ConcType.Color, 1),

				new TestItem("Battlefield", 4, ConcType.Game, 1),
				new TestItem("AssCreed", 4, ConcType.Game, 1),
				new TestItem("GTA", 24, ConcType.Game, 1),
				new TestItem("Minecraft", 10, ConcType.Game, 1),

				new TestItem("Banana", 1, ConcType.Fruit, 1),
				new TestItem("Apple", 1, ConcType.Fruit, 1),
				new TestItem("Pear", 2, ConcType.Fruit, 1),
			};

			var scheduler = new DynamicScheduler<TestItem, object>(items);
			scheduler.Execute("stuffs", Done, null);
		}

		private void Done(object obj)
		{
			Console.WriteLine("Schedule done");
		}
	}

	[DebuggerDisplay("{m_name}")]
	public class TestItem : IScheduleItem<TestItem, object>
	{
		private string m_name;
		private int m_sleepTime;

		public string Name => m_name;

		public ScheduleHint Hint => m_prio == 0 ? ScheduleHint.Early : ScheduleHint.Default;

		private ConcType m_concType;
		private int m_prio;

		public TestItem(string name, int sleepTime, ConcType concurrentType, int prio)
		{
			m_name = name;
			m_sleepTime = sleepTime;
			m_concType = concurrentType;
			m_prio = prio;
		}

		public override string ToString()
		{
			return m_name;
		}

		public void Execute(object arg)
		{
			Thread.Sleep(m_sleepTime);
		}

		public ScheduleOrder ScheduleAgainst(TestItem other)
		{
			if (m_prio > other.m_prio)
				return ScheduleOrder.RunAfter;
			if (other.m_prio > m_prio)
				return ScheduleOrder.RunBefore;

			if (other.m_concType == m_concType)
				return ScheduleOrder.AnyOrderNotConcurrent;

			return ScheduleOrder.AnyOrderConcurrent;
		}
	}

	public class Head : IDynamicOrdered
	{
		public DynamicOrder OrderAgainst(object other)
		{
			if (other.GetType().Name == "Neck")
				return DynamicOrder.RequireABeforeB;
			return DynamicOrder.PreferABeforeB;
		}
	}

	public class Neck : IDynamicOrdered
	{
		public DynamicOrder OrderAgainst(object other)
		{
			if (other.GetType().Name == "Head")
				return DynamicOrder.PreferBBeforeA;
			return DynamicOrder.PreferABeforeB;
		}
	}


	public class Torso : IDynamicOrdered
	{
		public DynamicOrder OrderAgainst(object other)
		{
			if (other.GetType().Name == "Feet")
				return DynamicOrder.RequireABeforeB;
			return DynamicOrder.AnyOrder;
		}
	}

	public class Legs : IDynamicOrdered
	{
		public DynamicOrder OrderAgainst(object other)
		{
			if (other.GetType().Name == "Torso")
				return DynamicOrder.RequireBBeforeA;
			return DynamicOrder.AnyOrder;
		}
	}

	public class Feet : IDynamicOrdered
	{
		public DynamicOrder OrderAgainst(object other)
		{
			if (other.GetType().Name == "Head")
				return DynamicOrder.RequireBBeforeA;
			return DynamicOrder.PreferBBeforeA;
		}
	}
}


<|start_filename|>src/File/FileCompare.cs<|end_filename|>
﻿using System;
using System.Buffers;
using System.IO;
using System.Runtime.InteropServices;

namespace Lidgren.Core
{
	public static class FileCompare
	{
		public enum Result
		{
			/// <summary>
			/// The files have identical contents
			/// </summary>
			Identical,

			/// <summary>
			/// The same file was passed in both arguments
			/// </summary>
			SameFile,

			/// <summary>
			/// The files have different contents
			/// </summary>
			Different,

			/// <summary>
			/// File1 does not exist
			/// </summary>
			File1Missing,

			/// <summary>
			/// File2 does not exist
			/// </summary>
			File2Missing,

			/// <summary>
			/// Neither File1 or File2 exists
			/// </summary>
			BothFilesMissing,
		}

		private const int kDefaultBufferSize = 1024 * 16;

		/// <summary>
		/// Compare content of two files; buffers must be equal length and multiple of 8
		/// </summary>
		public static Result AreEqual(string filename1, string filename2, int bufSize = kDefaultBufferSize)
		{
			var retval = CompareByFileName(filename1, filename2, out bool gotResult, out FileInfo f1, out FileInfo f2);
			if (gotResult)
				return retval;
			using (var fs1 = f1.OpenRead())
			{
				using (var fs2 = f2.OpenRead())
				{
					return AreEqual(fs1, fs2, bufSize);
				}
			}
		}

		/// <summary>
		/// Compare content of two files; buffers must be equal length and multiple of 8
		/// </summary>
		public static Result AreEqual(string filename1, string filename2, Span<byte> buf1, Span<byte> buf2)
		{
			var retval = CompareByFileName(filename1, filename2, out bool gotResult, out FileInfo f1, out FileInfo f2);
			if (gotResult)
				return retval;
			using (var fs1 = f1.OpenRead())
			{
				using (var fs2 = f2.OpenRead())
				{
					return AreEqual(fs1, fs2, buf1, buf2);
				}
			}
		}

		/// <summary>
		/// Compare content of two streams; bufSize must be multiple of 8
		/// </summary>
		public static Result AreEqual(Stream fs1, Stream fs2, int bufSize = kDefaultBufferSize)
		{
			var pool = ArrayPool<byte>.Shared;
			byte[] buffer = null;
			try
			{
				buffer = pool.Rent(bufSize * 2);
				return AreEqual(fs1, fs2, buffer.AsSpan(0, bufSize), buffer.AsSpan(bufSize));
			}
			finally
			{
				if (buffer != null)
					pool.Return(buffer);
			}
		}

		/// <summary>
		/// Compare content of two streams; buffers must be equal length and multiple of 8
		/// </summary>
		public static Result AreEqual(Stream fs1, Stream fs2, Span<byte> buf1, Span<byte> buf2)
		{
			CoreException.Assert(buf1.Length == buf2.Length);
			CoreException.Assert((buf1.Length & 7) == 0);

			ReadOnlySpan<ulong> ulBuf1 = MemoryMarshal.Cast<byte, ulong>(buf1);
			ReadOnlySpan<ulong> ulBuf2 = MemoryMarshal.Cast<byte, ulong>(buf2);

			for (; ; )
			{
				int len = fs1.Read(buf1);
				if (len < 1)
					return Result.Identical;

				int len2 = fs2.Read(buf2.Slice(0, len));

				if (len != len2)
					CoreException.Throw("Failed to read part of file");

				if (len == buf1.Length)
				{
					// compare ulongs
					if (ulBuf1.SequenceEqual(ulBuf2) == false)
						return Result.Different;
				}
				else
				{
					// compare bytes
					if (buf1.Slice(0, len).SequenceEqual(buf2.Slice(0, len)) == false)
						return Result.Different;
				}
			}
		}

		private static Result CompareByFileName(string filename1, string filename2, out bool gotResult, out FileInfo f1, out FileInfo f2)
		{
			bool m1 = string.IsNullOrEmpty(filename1);
			bool m2 = string.IsNullOrEmpty(filename2);
			if (m1)
			{
				f1 = f2 = null;
				gotResult = true;
				return m2 ? Result.BothFilesMissing : Result.File1Missing;
			}
			if (m2)
			{
				f1 = f2 = null;
				gotResult = true;
				return Result.File2Missing;
			}

			// check existence and file sizes
			f1 = new FileInfo(filename1);
			f2 = new FileInfo(filename2);

			if (f1.Exists == false)
			{
				gotResult = true;
				return (f2.Exists == false) ? Result.BothFilesMissing : Result.File1Missing;
			}
			if (f2.Exists == false)
			{
				gotResult = true;
				return Result.File2Missing;
			}

			if (f1.FullName.Equals(f2.FullName, StringComparison.Ordinal))
			{
				gotResult = true;
				return Result.SameFile;
			}

			gotResult = f1.Length != f2.Length;
			return Result.Different;
		}
	}
}



<|start_filename|>src/Timing/TimingThread.cs<|end_filename|>
﻿#nullable enable
using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Threading;

namespace Lidgren.Core
{
	[DebuggerDisplay("{Name} {Start} Dur {Duration}")]
	public struct TimingEntry
	{
		public string Name;
		public long Start;
		public uint Duration;
	}

	[DebuggerDisplay("{m_threadIndex} {m_threadName}")]
	public sealed class TimingThread
	{
		private const int k_maxCompletedEntries = 1024;
		private const int k_attemptFlushCount = 1024 - 64; // try to flush after this many completed entries

		private string? m_threadName;
		private int m_threadIndex;
		private readonly TimingEntry[] m_completed;
		private int m_completedCount;
		private bool m_triggerFlushOnce;
		private Thread m_thread;

		public string Name => m_threadName ?? "";
		public Thread Thread => m_thread;
		public int Index => m_threadIndex;

		/// <summary>
		/// One instance per thread
		/// </summary>
		[ThreadStatic]
		private static TimingThread? s_instance;

		/// <summary>
		/// Get thread specific timing instance
		/// </summary>
		public static TimingThread Instance
		{
			get
			{
				if (s_instance == null)
					s_instance = CreateNewTimingThread(); // thread static and rare
				return s_instance;
			}
		}

		// rare; don't inline!
		[MethodImpl(MethodImplOptions.NoInlining)]
		private static TimingThread CreateNewTimingThread()
		{
			var instance = new TimingThread();
			int index = TimingService.RegisterThread(instance);
			instance.Init(index);
			return instance;
		}

		public TimingThread()
		{
			m_completed = new TimingEntry[k_maxCompletedEntries];
			m_thread = Thread.CurrentThread;
		}

		internal void TriggerFlush()
		{
			m_triggerFlushOnce = true;
		}

		/// <summary>
		/// Only call manually when adding external or "virtual" thread such as a "GPU thread"
		/// </summary>
		public void Init(int threadIndex, string? threadName = null)
		{
			m_threadIndex = threadIndex;
			if (string.IsNullOrWhiteSpace(threadName))
			{
				threadName = m_thread?.Name;
				if (string.IsNullOrWhiteSpace(threadName))
					threadName = "Thread#" + threadIndex;
			}
			m_threadName = threadName;
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void AddEntry(string name, long started, int duration)
		{
			if (duration < 0)
			{
				CoreException.Assert(duration >= 0, "AddEntry negative duration");
				return;
			}

			int ccnt = m_completedCount;

			if (ccnt > 0 && m_completed[ccnt - 1].Start == started)
				started++; // push one tick to make better hierarchies

			// no need for interlocked; this is a thread local object
			ref var complete = ref m_completed[ccnt];
			m_completedCount = ccnt + 1;
			complete.Name = name;
			complete.Start = started;
			complete.Duration = (uint)duration;
			if (m_triggerFlushOnce || m_completedCount >= k_attemptFlushCount)
				InternalFlush();
		}

		// running on this thread; unless forced
		internal void InternalFlush()
		{
			lock (m_completed) // typically not needed; but cheap if not contended anyway
			{
				m_triggerFlushOnce = false;
				if (m_completedCount > 0)
				{
					TimingService.Flush(this, m_completed, m_completedCount);
					m_completedCount = 0;
				}
			}
		}
	}
}


<|start_filename|>src/Text/Tokenizer.cs<|end_filename|>
﻿#nullable enable
using System;

namespace Lidgren.Core
{
	public static class TokenizerExtensions
	{
		public static Tokenizer<char> Tokenize(this ReadOnlySpan<char> str)
		{
			return new Tokenizer<char>(str);
		}
	}

	/// <summary>
	/// Allocation free alternative to string.Split() working on Span<char> (actually span of anything)
	/// Usage:
	/// var tokenizer = "this is a string".Tokenize();
	/// while(tokenizer.GetNext(' ', out var span)
	///    HandleToken(span);
	/// </summary>
	public ref struct Tokenizer<T> where T : struct, IEquatable<T>
	{
		private ReadOnlySpan<T> m_all;
		private int m_currentIndex;

		public Tokenizer(ReadOnlySpan<T> data)
		{
			m_all = data;
			m_currentIndex = 0;
		}

		/// <summary>
		/// Gets remaining (untokenized) data
		/// </summary>
		public readonly ReadOnlySpan<T> Remaining => m_all.Slice(m_currentIndex);

		/// <summary>
		/// Follows same rules as string.Split(delimiter)
		/// </summary>
		public ReadOnlySpan<T> Next(T delimiter)
		{
			int idx = m_currentIndex;

			var all = m_all;
			if (idx > all.Length)
				return default(ReadOnlySpan<T>);

			for (; ; )
			{
				if (idx >= all.Length)
					break;
				if (all[idx].Equals(delimiter))
					break;
				idx++;
			}
			var token = all.Slice(m_currentIndex, idx - m_currentIndex);
			m_currentIndex = idx + 1;
			return token;
		}

		/// <summary>
		/// Follows same rules as string.Split(delimiters)
		/// </summary>
		public ReadOnlySpan<T> Next(ReadOnlySpan<T> delimiters)
		{
			int idx = m_currentIndex;

			var all = m_all;
			if (idx > all.Length)
				return default(ReadOnlySpan<T>);

			for (; ; )
			{
				if (idx >= all.Length)
					break;

				bool got = false;
				for (int i = 0; i < delimiters.Length; i++)
					got |= all[idx].Equals(delimiters[i]);
				if (got)
					break;
				idx++;
			}
			var token = all.Slice(m_currentIndex, idx - m_currentIndex);
			m_currentIndex = idx + 1;
			return token;
		}

		/// <summary>
		/// Follows same rules as string.Split(delimiter)
		/// </summary>
		public ReadOnlySpan<T> PeekNext(T delimiter)
		{
			int idx = m_currentIndex;

			var all = m_all;
			if (idx > all.Length)
				return default(ReadOnlySpan<T>);

			for (; ; )
			{
				if (idx >= all.Length)
					break;
				if (all[idx].Equals(delimiter))
					break;
				idx++;
			}
			var token = all.Slice(m_currentIndex, idx - m_currentIndex);
			return token;
		}

		/// <summary>
		/// Follows same rules as string.Split(delimiter)
		/// </summary>
		public bool GetNext(T delimiter, out ReadOnlySpan<T> token)
		{
			int idx = m_currentIndex;

			var all = m_all;
			if (idx > all.Length)
			{
				token = default(ReadOnlySpan<T>);
				return false;
			}

			for (; ; )
			{
				if (idx >= all.Length)
					break;
				if (all[idx].Equals(delimiter))
					break;
				idx++;
			}
			token = all.Slice(m_currentIndex, idx - m_currentIndex);
			m_currentIndex = idx + 1;
			return true;
		}

		/// <summary>
		/// Follows same rules as string.Split(delimiter)
		/// </summary>
		public bool GetNext(ReadOnlySpan<T> delimiters, out ReadOnlySpan<T> token)
		{
			int idx = m_currentIndex;

			var all = m_all;
			if (idx > all.Length)
			{
				token = default(ReadOnlySpan<T>);
				return false;
			}

			for (; ; )
			{
				if (idx >= all.Length)
					break;
				bool got = false;
				for (int i = 0; i < delimiters.Length; i++)
					got |= all[idx].Equals(delimiters[i]);
				if (got)
					break;
				idx++;
			}
			token = all.Slice(m_currentIndex, idx - m_currentIndex);
			m_currentIndex = idx + 1;
			return true;
		}

		public bool GetNextSkipEmpty(T delimiter, out ReadOnlySpan<T> token)
		{
			int idx = m_currentIndex;

			if (idx > m_all.Length)
			{
				token = default(ReadOnlySpan<T>);
				return false;
			}

			// skip delimiters
			for (; ; )
			{
				if (idx >= m_all.Length)
				{
					token = default(ReadOnlySpan<T>);
					return false;
				}
				if (m_all[idx].Equals(delimiter) == false)
					break;
				idx++;
			}
			m_currentIndex = idx;

			for (; ; )
			{
				if (idx >= m_all.Length)
					break;
				if (m_all[idx].Equals(delimiter))
					break;
				idx++;
			}
			token = m_all.Slice(m_currentIndex, idx - m_currentIndex);
			m_currentIndex = idx + 1;
			return token.Length > 0;
		}

		public ReadOnlySpan<T> NextSkipEmpty(T delimiter)
		{
			int idx = m_currentIndex;

			if (idx > m_all.Length)
				return default(ReadOnlySpan<T>);

			// skip delimiters
			for (; ; )
			{
				if (idx >= m_all.Length)
					return default(ReadOnlySpan<T>);
				if (m_all[idx].Equals(delimiter) == false)
					break;
				idx++;
			}
			m_currentIndex = idx;

			for (; ; )
			{
				if (idx >= m_all.Length)
					break;
				if (m_all[idx].Equals(delimiter))
					break;
				idx++;
			}
			var token = m_all.Slice(m_currentIndex, idx - m_currentIndex);
			m_currentIndex = idx + 1;
			return token;
		}

		/// <summary>
		/// Returns number of items, up to into.Length
		/// </summary>
		public static int Split(ReadOnlySpan<T> data, T delimiter, Span<Range> into)
		{
			int count = 0;
			int start = 0;
			int idx = 0;
			while(idx < data.Length)
			{
				if (data[idx].Equals(delimiter))
				{
					into[count++] = new Range(start, idx);
					if (count == into.Length)
						return count;
					start = idx + 1;
				}
				idx++;
			}
			if (idx > start)
				into[count++] = new Range(start, idx);
			else if (data[idx - 1].Equals(delimiter))
				into[count++] = new Range(idx - 1, idx - 1); // if ending in delimiter; add extra empty token

			return count;
		}

		public static bool SplitOnce(ReadOnlySpan<T> data, T delimiter, out ReadOnlySpan<T> first, out ReadOnlySpan<T> second)
		{
			var idx = data.IndexOf(delimiter);
			if (idx == -1)
			{
				first = default;
				second = default;
				return false;
			}
			first = data.Slice(0, idx);
			second = data.Slice(idx + 1);
			return true;
		}
	}
}


<|start_filename|>tests/PRNGTests.cs<|end_filename|>
﻿using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;

using Lidgren.Core;

namespace UnitTests
{
	[TestClass]
	public class PRNGTests
	{
		[TestMethod]
		public void TestPRNG()
		{
			const int iterations = 5000000;
			Console.WriteLine();

			int trues = 0;
			int larger = 0;
			int smaller = 0;
			for (int i = 0; i < iterations; i++)
			{
				if (PRNG.NextBool())
					trues++;
				int val = PRNG.Next(5, 8); // 5, 6 or 7
				switch (val)
				{
					case 5:
						smaller++;
						break;
					case 6:
						break;
					case 7:
						larger++;
						break;
					default:
						Assert.Fail("Value " + val + " isn't 5, 6 or 7");
						break;
				}

				// ranged
				var r1 = (int)PRNG.NextUInt32();
				var r2 = (int)PRNG.NextUInt32();
				if (r1 < r2)
				{
					var rr = PRNG.Next(r1, r2);
					Assert.IsTrue(rr >= r1 && rr < r2, rr.ToString() + " is not between " + r1 + " and " + r2);
				}
				else if (r1 > r2)
				{
					var rr = PRNG.Next(r2, r1);
					Assert.IsTrue(rr >= r2 && rr < r1, rr.ToString() + " is not between " + r2 + " and " + r1);
				}
			}

			var p = (double)trues / (double)iterations;
			Assert.IsTrue(p > 0.495 && p < 0.505);

			const double third = 1.0 / 3.0;
			const double offset = third * 0.05;
			const double low = third - offset;
			const double high = third + offset;

			p = (double)smaller / (double)iterations;
			Assert.IsTrue(p > low && p < high);

			p = (double)larger / (double)iterations;
			Assert.IsTrue(p > low && p < high);

			// make sure nextdouble() and nextfloat() don't generate numbers outside range
			var state = RandomSeed.GetUInt64();
			for (int r = 0; r < 10000000; r++)
			{
				double d = PRNG.NextDouble(ref state);
				Assert.IsTrue(d >= 0.0 && d < 1.0);

				float f = PRNG.NextFloat(ref state);
				Assert.IsTrue(f >= 0.0f && f < 1.0f);
			}
		}
	}
}


<|start_filename|>src/Text/ValueStringBuilder.cs<|end_filename|>
﻿#nullable enable
using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;

namespace Lidgren.Core
{
	/// <summary>
	/// Replacement for StringBuilder but it's a value type and takes spans
	/// </summary>
	public struct ValueStringBuilder
	{
		private static readonly bool s_crlf = Environment.NewLine.Equals("\n", StringComparison.Ordinal) ? false : true;
		private char[] m_buffer;
		private int m_length;

		public readonly int Length => m_length;
		public readonly Span<char> Span => m_buffer.AsSpan(0, m_length);
		public readonly ReadOnlySpan<char> ReadOnlySpan => m_buffer.AsSpan(0, m_length);

		public ValueStringBuilder(int initialCapacity)
		{
			m_buffer = new char[initialCapacity];
			m_length = 0;
		}

		public void Clear()
		{
			m_length = 0;
		}

		public int Capacity
		{
			readonly get { return m_buffer.Length; }
			set
			{
				var newBuffer = new char[value];
				m_buffer.AsSpan(0, m_length).CopyTo(newBuffer);
				m_buffer = newBuffer;
			}
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		private void EnsureCapacity(int add)
		{
			if (add > m_buffer.Length - m_length)
				Grow(add);
		}

		[MethodImpl(MethodImplOptions.NoInlining)]
		private void Grow(int add)
		{
			int newSize = Math.Max(m_length + add, m_buffer.Length * 2);
			Capacity = newSize;
		}

		// assumes capacity exists; returns number of chars added
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		private int NewLine(Span<char> span)
		{
			if (s_crlf)
			{
				span[0] = '\r';
				span[1] = '\n';
				return 2;
			}
			span[0] = '\n';
			return 1;
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void AppendLine()
		{
			if (m_buffer.Length - m_length < 2)
				Grow(8);
			int len = m_length;
			len += NewLine(m_buffer.AsSpan(len));
			m_length = len;
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void AppendLine(string str)
		{
			AppendLine(str.AsSpan());
		}

		public void AppendLine(ReadOnlySpan<char> str)
		{
			if (str.Length == 0)
			{
				AppendLine();
				return;
			}

			var curLen = m_length;

			int ensureSize = str.Length + 2; // +2 for max newline size
			if (ensureSize > m_buffer.Length - m_length)
				Grow(ensureSize);

			var span = m_buffer.AsSpan(curLen);
			str.CopyTo(span);
			span = span.Slice(str.Length);
			var nllen = NewLine(span);
			m_length = curLen + str.Length + nllen;
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void Append(string str)
		{
			Append(str.AsSpan());
		}

		public void Append(ReadOnlySpan<char> str)
		{
			if (str.Length == 0)
				return;
			EnsureCapacity(str.Length);
			var span = m_buffer.AsSpan(m_length);
			str.CopyTo(span);
			m_length += str.Length;
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void Append(char c)
		{
			EnsureCapacity(1);
			m_buffer[m_length++] = c;
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void AppendLine(char c)
		{
			Append(c);
			AppendLine();
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void Append(bool value)
		{
			EnsureCapacity(5);
			bool ok = value.TryFormat(m_buffer.AsSpan(m_length), out int written);
			CoreException.Assert(ok);
			m_length += written;
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void AppendLine(bool value)
		{
			Append(value);
			AppendLine();
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void Append(int value)
		{
			EnsureCapacity(12);
			bool ok = value.TryFormat(m_buffer.AsSpan(m_length), out int written);
			CoreException.Assert(ok);
			m_length += written;
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void AppendLine(int value)
		{
			Append(value);
			AppendLine();
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void Append(uint value)
		{
			EnsureCapacity(12);
			bool ok = value.TryFormat(m_buffer.AsSpan(m_length), out int written);
			CoreException.Assert(ok);
			m_length += written;
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void AppendLine(uint value)
		{
			Append(value);
			AppendLine();
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void Append(byte value)
		{
			EnsureCapacity(4);
			bool ok = value.TryFormat(m_buffer.AsSpan(m_length), out int written);
			CoreException.Assert(ok);
			m_length += written;
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void AppendLine(byte value)
		{
			Append(value);
			AppendLine();
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void Append(short value)
		{
			EnsureCapacity(7);
			bool ok = value.TryFormat(m_buffer.AsSpan(m_length), out int written);
			CoreException.Assert(ok);
			m_length += written;
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void AppendLine(short value)
		{
			Append(value);
			AppendLine();
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void Append(ushort value)
		{
			EnsureCapacity(6);
			bool ok = value.TryFormat(m_buffer.AsSpan(m_length), out int written);
			CoreException.Assert(ok);
			m_length += written;
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void AppendLine(ushort value)
		{
			Append(value);
			AppendLine();
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void Append(long value)
		{
			EnsureCapacity(22);
			bool ok = value.TryFormat(m_buffer.AsSpan(m_length), out int written);
			CoreException.Assert(ok);
			m_length += written;
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void AppendLine(long value)
		{
			Append(value);
			AppendLine();
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void Append(ulong value)
		{
			EnsureCapacity(22);
			bool ok = value.TryFormat(m_buffer.AsSpan(m_length), out int written);
			CoreException.Assert(ok);
			m_length += written;
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void AppendLine(ulong value)
		{
			Append(value);
			AppendLine();
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void Append(float value)
		{
			EnsureCapacity(24);
			bool ok = value.TryFormat(m_buffer.AsSpan(m_length), out int written);
			CoreException.Assert(ok);
			m_length += written;
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void AppendLine(float value)
		{
			Append(value);
			AppendLine();
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void Append(double value)
		{
			EnsureCapacity(24);
			bool ok = value.TryFormat(m_buffer.AsSpan(m_length), out int written);
			CoreException.Assert(ok);
			m_length += written;
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void AppendLine(double value)
		{
			Append(value);
			AppendLine();
		}

		/// <summary>
		/// Returns number of characters replaced
		/// </summary>
		public int Replace(char oldChar, char newChar)
		{
			var content = this.Span;
			int retval = 0;
			for (int i = 0; i < content.Length; i++)
			{
				if (content[i] == oldChar)
				{
					content[i] = newChar;
					retval++;
				}
			}
			return retval;
		}

		/// <summary>
		/// Returns number of substitutions
		/// </summary>
		public int Replace(string oldValue, string newValue)
		{
			int retval = 0;

			if (oldValue.Length == newValue.Length)
			{
				// in-place replacement
				var span = this.Span;
				for (; ; )
				{
					var idx = span.IndexOf(oldValue);
					if (idx == -1)
						break;
					newValue.AsSpan().CopyTo(span.Slice(idx, newValue.Length));
					retval++;
					span = span.Slice(idx + 1); // avoid infinite substitutions
				}
				return retval;
			}

			for(; ;)
			{
				var span = this.Span;
				var idx = span.IndexOf(oldValue);
				if (idx == -1)
					break;
				Remove(idx, oldValue.Length);
				Insert(idx, newValue);
				retval++;
			}
			return retval;
		}

		public void Insert(int index, ReadOnlySpan<char> value)
		{
			EnsureCapacity(value.Length);
			if (index == m_length)
			{
				Append(value);
				return;
			}

			if (index == 0)
			{
				this.ReadOnlySpan.CopyTo(m_buffer.AsSpan(value.Length));
				value.CopyTo(m_buffer.AsSpan(0, value.Length));
				m_length += value.Length;
				return;
			}

			// in the middle
			var tail = ReadOnlySpan.Slice(index);
			tail.CopyTo(m_buffer.AsSpan(index + value.Length));
			value.CopyTo(m_buffer.AsSpan(index));
			m_length += value.Length;
		}

		public void Remove(int index, int length)
		{
			int tail = m_length - (index + length);
			CoreException.Assert(tail >= 0);
			m_length -= length;
			if (tail == 0)
				return;
			m_buffer.AsSpan(index + length, tail).CopyTo(m_buffer.AsSpan(index, tail));
		}

		public override int GetHashCode()
		{
			return (int)HashUtil.Hash32(ReadOnlySpan);
		}

		public override string ToString()
		{
			return ReadOnlySpan.ToString();
		}
	}
}


<|start_filename|>src/Collections/FastList/FastList.cs<|end_filename|>
﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

namespace Lidgren.Core
{
	/// <summary>
	/// List of T alternative with the following properties
	/// * Underlying array is accessible (remember to respect m_offset and Count property)
	/// * ref return methods
	/// * pop/trypop
	/// * Very little validation; except in DEBUG
	/// * Best iterated over myFastList.ReadOnlySpan
	/// </summary>
	[DebuggerDisplay("FastList<{typeof(T).Name}> {Count}/{Capacity}")]
	public partial class FastList<T>
	{
		private T[] m_buffer = System.Array.Empty<T>();
		private int m_offset;
		private int m_count;

		public int Count
		{
			get { return m_count; }
			set
			{
				CoreException.Assert(value <= m_buffer.Length);
				m_count = value;
				if (m_offset + m_count > m_buffer.Length)
					Compact(); // ensure Span can operate
			}
		}

		public int Capacity
		{
			[MethodImpl(MethodImplOptions.AggressiveInlining)]
			get { return m_buffer.Length; }
			[MethodImpl(MethodImplOptions.NoInlining)]
			set
			{
				if (value == 0)
				{
					m_buffer = System.Array.Empty<T>();
					m_offset = 0;
					m_count = 0;
					return;
				}

				if (value == m_buffer.Length)
					return;

				var newItems = new T[value];
				m_count = Math.Min(m_count, value);
				if (m_count > 0)
					ReadOnlySpan.Slice(0, m_count).CopyTo(newItems);
				m_buffer = newItems;
				m_offset = 0;
			}
		}

		// cold path, never inline
		[MethodImpl(MethodImplOptions.NoInlining)]
		private void Compact()
		{
			CoreException.Assert(m_offset > 0, "Unnecessary compact!");
			var buffer = m_buffer;
			var off = m_offset;
			var cnt = m_count;
			// TODO: compare to aliased Span.CopyTo
			for (int i = 0; i < cnt; i++)
				buffer[i] = buffer[off + i];
			m_offset = 0;
		}

		/// <summary>
		/// May compact buffer if it's offset
		/// </summary>
		public T[] GetBuffer()
		{
			if (m_offset != 0)
				Compact();
			return m_buffer;
		}

		/// <summary>
		/// Replace backing items array and set count
		/// </summary>
		public void ReplaceBuffer(T[] items, int count)
		{
			m_buffer = items;
			m_offset = 0;
			m_count = count;
		}

		/// <summary>
		/// Get readonly span of items in list
		/// </summary>
		public ReadOnlySpan<T> ReadOnlySpan => new ReadOnlySpan<T>(m_buffer, m_offset, m_count);

		/// <summary>
		/// Get span of items in list
		/// </summary>
		public Span<T> Span => new Span<T>(m_buffer, m_offset, m_count);

		/// <summary>
		/// Get memory span of items in list
		/// </summary>
		public Memory<T> Memory => new Memory<T>(m_buffer, m_offset, m_count);

		public FastList(int initialCapacity = 0)
		{
			if (initialCapacity == 0)
				m_buffer = System.Array.Empty<T>();
			else
				m_buffer = new T[initialCapacity];
			m_count = 0;
			m_offset = 0;
		}

		public FastList(ReadOnlySpan<T> copyItems)
		{
			m_buffer = copyItems.ToArray();
			m_count = copyItems.Length;
			m_offset = 0;
		}

		public FastList(IEnumerable<T> copyItems)
		{
			// loop over this twice to avoid unnecessary garbage
			int cnt = 0;
			foreach (var _ in copyItems)
				cnt++;
			m_count = 0;
			m_offset = 0;
			if (cnt == 0)
			{
				m_buffer = System.Array.Empty<T>();
			}
			else
			{
				m_buffer = new T[cnt];
				AddRangeSlow(copyItems);
			}
		}

		private FastList()
		{
		}

		public static FastList<T> CreateFromBackingArray(T[] arr, int initialCount)
		{
			var retval = new FastList<T>();
			retval.m_count = initialCount;
			retval.m_buffer = arr;
			retval.m_offset = 0;
			return retval;
		}

		public static implicit operator ReadOnlySpan<T>(FastList<T> list)
		{
			return list.ReadOnlySpan;
		}
	}
}


<|start_filename|>src/JobService/JobService.Enqueue.cs<|end_filename|>
﻿using System;
using System.Runtime.CompilerServices;
using System.Threading;

namespace Lidgren.Core
{
	public static partial class JobService
	{
		private const int k_maxInFlightJobs = 1024; // must be power of two
		private const int k_instancesMask = k_maxInFlightJobs - 1;

		private static readonly Job[] s_instances = new Job[k_maxInFlightJobs];
		private static int s_nextInstance;
		private static int s_instancesCount;

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		internal static void EnqueueInternal(string name, Action<object> work, object argument, JobCompletion completion = null)
		{
#if DEBUG
			CoreException.Assert(Monitor.IsEntered(s_instances));
#endif

			int idx = (s_nextInstance + s_instancesCount) & k_instancesMask;
			ref var job = ref s_instances[idx];
			job.Name = name;
			job.Work = work;
			job.Argument = argument;
			job.Completion = completion;
			s_instancesCount++;
			CoreException.Assert(s_instancesCount <= s_instances.Length);
			JobWait.Set();
		}

		/// <summary>
		/// Enqueue work to be done as soon as possible; returns immediately
		/// </summary>
		public static void Enqueue(string name, Action<object> work, object argument)
		{
			CoreException.Assert(s_workers != null, "JobService not initialized");
			lock(s_instances)
				EnqueueInternal(name, work, argument, null);
		}

		/// <summary>
		/// Enqueue work to be done as soon as possible; returns immediately
		/// </summary>
		public static void Enqueue(Action<object> work, object argument = null)
		{
			CoreException.Assert(s_workers != null, "JobService not initialized");
			lock(s_instances)
				EnqueueInternal("unnamed", work, argument, null);
		}

		/// <summary>
		/// Enqueue work to be done as soon as possible; returns immediately. When work is completed, continuation runs with continuationArgument.
		/// </summary>
		public static void Enqueue(string name, Action<object> work, object argument, Action<object> continuation, object continuationArgument = null, string continuationName = "continuation")
		{
			CoreException.Assert(s_workers != null, "JobService not initialized");
			var completion = JobCompletion.Acquire();
			completion.Continuation = continuation;
			completion.ContinuationArgument = continuationArgument;
			completion.ContinuationAtCount = 1;
#if DEBUG
			completion.ContinuationName = string.IsNullOrWhiteSpace(continuationName) ? name + "Contd" : continuationName;
#else
			completion.ContinuationName = continuationName;
#endif

			lock(s_instances)
				EnqueueInternal(name, work, argument, completion);
		}

		/// <summary>
		/// Enqueue work to be run (available threads) times; returns immediately.
		/// </summary>
		public static void EnqueueWide(string name, Action<object> work, object argument)
		{
			CoreException.Assert(s_workers != null, "JobService not initialized");
			lock (s_instances)
			{
				for (int i = 0; i < s_workers.Length; i++)
					EnqueueInternal(name, work, argument, null);
			}
		}

		/// <summary>
		/// Enqueue work to be run (available threads) times concurrently; blocks until all has completed
		/// </summary>
		public static void EnqueueWideBlock(string name, Action<object> work, object argument)
		{
			EnqueueWideBlock(int.MaxValue, name, work, argument);
		}

		/// <summary>
		/// Enqueue work to be run (available threads, but max maxConcurrency) times concurrently; blocks until all has completed
		/// </summary>
		public static void EnqueueWideBlock(int maxConcurrency, string name, Action<object> work, object argument)
		{
			CoreException.Assert(s_workers != null, "JobService not initialized");
			int numJobs = s_workers.Length - 1; // -1 because we assume local thread is one of the worker threads
			numJobs = Math.Min(numJobs, maxConcurrency - 1);

			var completion = JobCompletion.Acquire();
			completion.ContinuationAtCount = -1;
			lock (s_instances)
			{
				for (int i = 0; i < numJobs; i++)
					EnqueueInternal(name, work, argument, completion);
			}
			work(argument); // do one run on this thread as well
			completion.WaitAndRelease(numJobs);
		}

		/// <summary>
		/// Enqueue work to be run (available threads) times concurrently; calls continuation(continuationArgument) when all completed
		/// </summary>
		public static void EnqueueWide(string name, Action<object> work, object argument, Action<object> continuation, object continuationArgument)
		{
			CoreException.Assert(s_workers != null, "JobService not initialized");
			int numJobs = s_workers.Length;

			var completion = JobCompletion.Acquire();
			completion.ContinuationAtCount = numJobs;
			completion.Continuation = continuation;
			completion.ContinuationArgument = continuationArgument;
#if DEBUG
			completion.ContinuationName = name + "Contd";
#endif
			lock (s_instances)
			{
				for (int i = 0; i < numJobs; i++)
					EnqueueInternal(name, work, argument, completion);
			}
		}
	}
}


<|start_filename|>src/JobService/JobService.cs<|end_filename|>
﻿using System;
using System.Diagnostics;
using System.Threading;

namespace Lidgren.Core
{
	public static partial class JobService
	{
		private static JobWorker[] s_workers;
		private static object s_setupShutdownLock = new object();

		public static int WorkersCount => s_workers == null ? 0 : s_workers.Length;

		public static bool IsInitialized => s_workers != null;

		internal static AutoResetEvent JobWait = new AutoResetEvent(true);
		internal static int m_idleWorkers;

		/// <summary>
		/// returns worker index, or -1 if not jobservice thread
		/// </summary>
		public static int GetWorkerForCurrentThread()
		{
			var worker = JobWorker.WorkerForThread;
			return worker == null ? -1 : worker.Index;
		}

		public static void Initialize(int numWorkers = -1)
		{
			using var _ = new Timing("jobsvcinit");

			lock (s_setupShutdownLock)
			{
				if (s_workers != null)
					return; // already initialized

				// set up a good amount of workers; but leave some room for main thread and misc other threads
				var hwThreads = Environment.ProcessorCount;

				// minimum 2 job workers; some may assume at least some concurrency
				if (numWorkers == -1)
					numWorkers = (hwThreads <= 2) ? 2 : hwThreads - (1 + (hwThreads / 8));

				s_workers = new JobWorker[numWorkers];
				for (int i = 0; i < numWorkers; i++)
					s_workers[i] = new JobWorker(i);
			}
		}

		// execute one job on this thread (owned by worker)
		// return true if a job was found and executed
		internal static bool ExecuteAnyJob(JobWorker worker, JobCompletion requiredCompletion = null)
		{
			Job job;

			if (requiredCompletion != null)
			{
				if (StealJob(requiredCompletion, out job) == false)
					return false;
			}
			else
			{
				if (s_instancesCount < 0 || PopAnyJob(out job) == false)
				{
					if (PopDelayed(out job) == false)
						return false;
				}
			}

			// do job
			using (new Timing(job.Name))
			{
#if DEBUG
				if (worker != null)
					worker.CurrentJob = job;
#endif
				// go go go
				job.Work(job.Argument);
			}

			var cmp = job.Completion;
			if (cmp != null)
				cmp.IncrementCompleted();

			return true;
		}

		private static bool PopAnyJob(out Job job)
		{
			lock (s_instances)
			{
				int cnt = s_instancesCount;
				if (cnt <= 0)
				{
					job = default;
					return false; // exit quickly to let go of lock
				}

				// copy job description
				int nxt = s_nextInstance;
				job = s_instances[nxt];
				nxt = (nxt + 1) & k_instancesMask;
				s_nextInstance = nxt;

				var remainingInstances = cnt - 1;
				s_instancesCount = remainingInstances;
				if (remainingInstances > 0)
					JobWait.Set();
				return true;
			}
		}

		/// <summary>
		/// Slower than pop; will try to steal ONLY jobs with a certain completion
		/// </summary>
		private static bool StealJob(JobCompletion completion, out Job job)
		{
			lock (s_instances)
			{
				int cnt = s_instancesCount;
				if (cnt <= 0)
				{
					job = default;
					return false;
				}

				var idx = s_nextInstance;
				for (int i = 0; i < cnt; i++)
				{
					ref readonly var test = ref s_instances[idx];
					if (test.Completion == completion)
					{
						// found!
						job = test;

						// close gap
						for (; i < cnt - 1; i++)
						{
							var nxt = (idx + 1) & k_instancesMask;
							s_instances[idx] = s_instances[nxt];
							idx = nxt;
						}
						s_instancesCount = cnt - 1;
						return true;
					}
					idx = (idx + 1) & k_instancesMask;
				}

				job = default;
				return false;
			}
		}

		public static void Shutdown()
		{
			lock (s_setupShutdownLock)
			{
				if (s_workers != null)
				{
					foreach (var worker in s_workers)
						worker.SetState(JobWorkerState.Shutdown);
					s_workers = null;
					Thread.Sleep(50); // give workers time to exit orderly
				}
			}
		}
	}
}


<|start_filename|>tests/MiscTests.cs<|end_filename|>
﻿using System;
using Lidgren.Core;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace UnitTests
{
	[TestClass]
	public partial class MiscTests
	{
		[TestMethod]
		public void TestFourCC()
		{
			var v1 = new FourCC("avc1");
			Assert.AreEqual(0x31637661u, v1.Value);
			Assert.AreEqual("avc1", v1.ToString());

			var v2 = new FourCC('a', 'v', 'c', '1');
			Assert.AreEqual(0x31637661u, v2.Value);
			Assert.AreEqual("avc1", v2.ToString());

			var v3 = new FourCC(new byte[] { (byte)'a', (byte)'v', (byte)'c', (byte)'1' });
			Assert.AreEqual(0x31637661u, v3.Value);
			Assert.AreEqual("avc1", v3.ToString());

			var v4 = new FourCC(0x31637661u);
			Assert.AreEqual(0x31637661u, v4.Value);
			Assert.AreEqual("avc1", v4.ToString());
		}

		[TestMethod]
		public void TestRandomSeed()
		{
			// since it's random; these != 0 checks MAY THEORETICALLY fail; but the risk should be 1 in uint.maxvalue
			var a = RandomSeed.GetUInt32();
			Assert.IsTrue(a != 0);
			var b = RandomSeed.GetUInt64();
			Assert.IsTrue(b != 0);
			var expanded = RandomSeed.ExpandSeed(123);
			Assert.IsTrue((expanded & 0b11111111_11111111_11111111_11111111_00000000_00000000_00000000_00000000ul) != 0);
		}

		[TestMethod]
		public void TestHasher()
		{
			var hasher = Hasher.Create();
			hasher.Add((byte)0);
			hasher.Add((uint)42 << 5);
			hasher.Add((ulong)42 << 48);
			hasher.Add("string");
			hasher.Add('c');
			hasher.Add(new byte[] { 1, 2, 3, 4 });
			hasher.Finalize64();

			var h1 = Hasher.Create();
			h1.Add((byte)42);
			h1.Add((byte)43);
			h1.Add((byte)44);
			var a1 = h1.Finalize64();

			var h2 = Hasher.Create();
			h2.Add(new byte[] { (byte)42, (byte)43, (byte)44 });
			var a2 = h2.Finalize64();

			Assert.AreEqual(a1, a2);
		}

		[TestMethod]
		public void TestBlockSwap()
		{
			var span = "1234BBB".ToCharArray().AsSpan();
			span.SwapBlocks(4);
			Assert.AreEqual("BBB1234", span.ToString());

			span = "12abcdefg".ToCharArray().AsSpan();
			span.SwapBlocks(1);
			Assert.AreEqual("2abcdefg1", span.ToString());

			span = "1234".ToCharArray().AsSpan();
			span.SwapBlocks(0);
			Assert.AreEqual("1234", span.ToString());

			span = "1234".ToCharArray().AsSpan();
			span.SwapBlocks(4);
			Assert.AreEqual("1234", span.ToString());

			span = "1234".ToCharArray().AsSpan();
			span.SwapBlocks(3);
			Assert.AreEqual("4123", span.ToString());
		}
	}
}


<|start_filename|>tests/StringBuilderTests.cs<|end_filename|>
﻿using System;
using Lidgren.Core;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace UnitTests
{
	[TestClass]
	public partial class StringBuilderTests
	{
		[TestMethod]
		public void TestStringBuilders()
		{
			{
				Span<char> buf = stackalloc char[128];
				var bdr = new FixedStringBuilder(buf);
				bdr.AppendLine("Hello World");
				bdr.Append("Hello");
				bdr.Append(' ');
				bdr.Append(12);
				bdr.Append(' ');
				bdr.AppendLine("Worlds");
				int numReplaced = bdr.Replace('e', 'u');
				Assert.AreEqual(2, numReplaced);

				var expected = "Hullo World" + Environment.NewLine + "Hullo 12 Worlds" + Environment.NewLine;
				Assert.AreEqual(expected.Length, bdr.Length);
				Assert.IsTrue(bdr.ReadOnlySpan.SequenceEqual(expected));

				numReplaced = bdr.Replace("Hullo", "Hi");
				Assert.AreEqual(2, numReplaced);
				expected = expected.Replace("Hullo", "Hi");
				Assert.IsTrue(bdr.ReadOnlySpan.SequenceEqual(expected));

				numReplaced = bdr.Replace("Hi", "");
				Assert.AreEqual(2, numReplaced);
				expected = expected.Replace("Hi", "");
				Assert.IsTrue(bdr.ReadOnlySpan.SequenceEqual(expected));

				bdr.Replace("World", "World");
				Assert.IsTrue(bdr.ReadOnlySpan.SequenceEqual(expected));
				bdr.Replace("Florka", "--");
				Assert.IsTrue(bdr.ReadOnlySpan.SequenceEqual(expected));
				bdr.Replace("Florka", "Florka");
				Assert.IsTrue(bdr.ReadOnlySpan.SequenceEqual(expected));

				bdr.Clear();
				Assert.AreEqual(0, bdr.Length);
				Assert.AreEqual("", bdr.ToString());
				bdr.Append("Koko");
				bdr.Append('s');
				Assert.AreEqual("Kokos".Length, bdr.Length);
				Assert.IsTrue(bdr.ReadOnlySpan.SequenceEqual("Kokos"));
			}

			{
				var sbdr = new ValueStringBuilder(8);
				sbdr.AppendLine("Hello World");
				sbdr.Append("Hello");
				sbdr.Append(' ');
				sbdr.Append(12u);
				sbdr.Append(' ');
				sbdr.AppendLine("Worlds");
				var numReplaced = sbdr.Replace('e', 'u');
				Assert.AreEqual(2, numReplaced);

				var expected = "Hullo World" + Environment.NewLine + "Hullo 12 Worlds" + Environment.NewLine;
				Assert.AreEqual(expected.Length, sbdr.Length);
				Assert.IsTrue(sbdr.ReadOnlySpan.SequenceEqual(expected));

				numReplaced = sbdr.Replace("Hullo", "Hi");
				Assert.AreEqual(2, numReplaced);
				expected = expected.Replace("Hullo", "Hi");
				Assert.IsTrue(sbdr.ReadOnlySpan.SequenceEqual(expected));

				numReplaced = sbdr.Replace("Hi", "");
				Assert.AreEqual(2, numReplaced);
				expected = expected.Replace("Hi", "");
				Assert.IsTrue(sbdr.ReadOnlySpan.SequenceEqual(expected));

				sbdr.Replace("World", "World");
				Assert.IsTrue(sbdr.ReadOnlySpan.SequenceEqual(expected));
				sbdr.Replace("Florka", "--");
				Assert.IsTrue(sbdr.ReadOnlySpan.SequenceEqual(expected));
				sbdr.Replace("Florka", "Florka");
				Assert.IsTrue(sbdr.ReadOnlySpan.SequenceEqual(expected));

				sbdr.Clear();
				Assert.AreEqual(0, sbdr.Length);
				Assert.AreEqual("", sbdr.ToString());
				sbdr.Append("Koko");
				sbdr.Append('s');
				Assert.AreEqual("Kokos".Length, sbdr.Length);
				Assert.IsTrue(sbdr.ReadOnlySpan.SequenceEqual("Kokos"));
			}
		}
	}
}


<|start_filename|>src/Collections/SymmetricMatrix.cs<|end_filename|>
﻿using System;
using System.Runtime.CompilerServices;

namespace Lidgren.Core
{
	/// <summary>
	/// Symmetrix matrix where [3, 5] references the same data as [5, 3] - using ~half the storage of a full matrix
	/// </summary>
	public class SymmetricMatrix<T>
	{
		public readonly T[] Data;

		public SymmetricMatrix(int size)
		{
			Data = new T[GetLength(size)];
		}

		public void Clear()
		{
			Data.AsSpan().Clear();
		}

		public T this[int x, int y]
		{
			[MethodImpl(MethodImplOptions.AggressiveInlining)]
			get
			{
				return Data[GetIndex(x, y)];
			}
			[MethodImpl(MethodImplOptions.AggressiveInlining)]
			set
			{
				Data[GetIndex(x, y)] = value;
			}
		}

		/// <summary>
		/// Gets the necessary array length to store a symmetric matrix of size * size
		/// </summary>
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static int GetLength(int size)
		{
			return ((size * size) + size) / 2;
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static int GetIndex(int x, int y)
		{
			if (y > x)
				return (((y * y) + y) / 2) + x;
			else
				return (((x * x) + x) / 2) + y;
		}
	}
}


<|start_filename|>tests/FastListTests.cs<|end_filename|>
﻿using System;
using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting;

using Lidgren.Core;

namespace UnitTests
{
	[TestClass]
	public class FastListTests
	{
		[TestMethod]
		public void TestFastList()
		{
			var facit = new List<int>(4);
			var fast = new FastList<int>();
			var repro = new FastList<int>();

			int[] range = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
			int[] smallRange = new int[] { 11, 12, 13 };

			Predicate<int> match = (a) =>
			{
				return a > 50000;
			};

			// run a million operations
			for (int rounds = 0; rounds < 1000000; rounds++)
			{
				// random operations!
				var op = PRNG.Next(0, 19);
				int removeIndex = PRNG.Next(0, Math.Max(1, facit.Count));
				var value = PRNG.Next(0, 100000);
				switch (op)
				{
					case 0:
						facit.Add(value);
						fast.Add(value);
						repro.Add(value);
						break;
					case 1:
						{
							facit.Add(value);
							ref var slot2 = ref fast.Add();
							slot2 = value;

							Verify(facit, fast);

							ref var slot3 = ref repro.Add();
							slot3 = value;
							break;
						}
					case 2:
						{
							facit.Add(value);
							ref var slot2 = ref fast.AddUninitialized();
							slot2 = value;
							ref var slot3 = ref repro.AddUninitialized();
							slot3 = value;
							break;
						}
					case 3:
						if (facit.Count > 0)
						{
							facit.RemoveAt(removeIndex);
							fast.RemoveAt(removeIndex);
							repro.RemoveAt(removeIndex);
						}
						break;
					case 4:
						if (facit.Count > 0)
						{
							int removeValue = facit[removeIndex];
							facit.Remove(removeValue);
							fast.Remove(removeValue);
							Verify(facit, fast);
							repro.Remove(removeValue);
						}
						break;
					case 5:
						facit.Clear();
						fast.Clear();
						repro.Clear();
						break;
					case 6:
						facit.Clear();
						fast.Clear();
						repro.Clear();
						break;
					case 7:
						{
							facit.AddRange(range);
							fast.AddRange(range);
							repro.AddRange(range);
						}
						break;
					case 8:
						{
							facit.AddRange(range);

							var span2 = fast.AddRangeUninitialized(range.Length);
							range.AsSpan().CopyTo(span2);

							var span3 = repro.AddRangeUninitialized(range.Length);
							range.AsSpan().CopyTo(span3);
						}
						break;
					case 9:
						{
							if (facit.Count > 0)
							{
								int index = PRNG.Next(0, facit.Count + 1); // can insert after all existing indices as well
								facit.Insert(index, value);
								fast.Insert(index, value);
								repro.Insert(index, value);
							}
						}
						break;
					case 10:
						{
							if (facit.Count > 0)
							{
								int index = PRNG.Next(0, facit.Count);
								facit.Insert(index, value);
								ref var slot2 = ref fast.InsertUninitialized(index);
								slot2 = value;
								ref var slot3 = ref repro.InsertUninitialized(index);
								slot3 = value;
							}
						}
						break;
					case 11:
						if (facit.Count > 0)
						{
							int index = PRNG.Next(0, facit.Count);
							facit[index] = value;
							fast[index] = value;
							repro[index] = value;
						}
						break;
					case 12:
						if (facit.Count > 0)
						{
							facit.RemoveAll(match);
							fast.RemoveAll(match);
							repro.RemoveAll(match);
						}
						break;
					case 13:
						facit.Add(value);
						fast.Add(value);
						repro.Add(value);
						break;
					case 14:
						facit.Insert(0, value);
						ref var zeroIns = ref fast.InsertUninitialized(0);
						zeroIns = value;
						ref var zeroIns2 = ref repro.InsertUninitialized(0);
						zeroIns2 = value;
						break;
					case 15:
						if (facit.Count > 0)
						{
							int lastValue = facit[facit.Count - 1];
							facit.RemoveAt(facit.Count - 1);
							int cmp = fast.Pop();
							Assert.IsTrue(lastValue == cmp);

							Verify(facit, fast);

							repro.Pop();
						}
						break;

					case 16:
						{
							facit.AddRange(smallRange);
							var span = fast.AddRangeUninitialized(smallRange.Length);
							smallRange.CopyTo(span);
							var span2 = repro.AddRangeUninitialized(smallRange.Length);
							smallRange.CopyTo(span2);
						}
						break;

					case 17:
						if (facit.Count > 0)
						{
							int lastValue = facit[facit.Count - 1];
							facit.RemoveAt(facit.Count - 1);
							bool ok = fast.TryPop(out var cmp);
							Assert.IsTrue(ok);
							Assert.IsTrue(lastValue == cmp);

							Verify(facit, fast);

							repro.TryPop(out var cmp2);
						}
						else
						{
							bool ok = fast.TryPop(out var unused);
							Assert.IsTrue(ok == false);
						}
						break;

					case 18:
						if (facit.Count > 3)
						{
							int idx = PRNG.Next(0, facit.Count);
							int maxRem = facit.Count - idx;
							int remCnt = PRNG.Next(0, maxRem + 1);

							facit.RemoveRange(idx, remCnt);
							fast.RemoveRange(idx, remCnt);
							Verify(facit, fast);
							repro.RemoveRange(idx, remCnt);
						}
						break;
				}

				// verify fast
				Verify(facit, fast);

				// check random index
				if (facit.Count > 0)
				{
					int rndIdx = PRNG.Next(0, facit.Count);
					var val = facit[rndIdx];

					// verify indexer
					Assert.IsTrue(fast[rndIdx] == val);

					// verify indexof
					var correct = facit.IndexOf(val);
					Assert.IsTrue(fast.IndexOf(val) == correct);

					// verify lastindexof
					correct = facit.LastIndexOf(val);
					Assert.IsTrue(fast.LastIndexOf(val) == correct);

					// verify contains
					Assert.IsTrue(facit.Contains(val));
					Assert.IsTrue(fast.Contains(val));
				}
			}
		}

		private void Verify(List<int> facit, FastList<int> fast)
		{
			Assert.AreEqual(facit.Count, fast.Count);
			var fastItems = fast.ReadOnlySpan;
			for (int i = 0; i < fastItems.Length; i++)
				Assert.AreEqual(facit[i], fastItems[i]);
			Assert.AreEqual(fast.ReadOnlySpan.Length, fast.Count);
			Assert.IsTrue(fast.Capacity >= fast.Count);

			for (int i = 0; i < fastItems.Length; i++)
				Assert.AreEqual(facit[i], fastItems[i]);
		}

		[TestMethod]
		public void TestAddList()
		{
			var add = new AddList<int>();
			add.Add(12);
			add.Add(13);
			add.Add(14);
			Assert.AreEqual(3, add.Count);
			var span = add.ReadOnlySpan;
			Assert.AreEqual(12, span[0]);
			Assert.AreEqual(13, span[1]);
			Assert.AreEqual(14, span[2]);
			Assert.AreEqual(3, span.Length);
		}

		[TestMethod]
		public void TestGroupBy()
		{
			var list = new FastList<int>();
			list.AddRange(new int[] { 1, 2, 3, 4, 5, 6, 9, 8, 7 });

			var dict = list.GroupBy<string>((x) => ((x & 1) == 0) ? "even" : "odd");

			Assert.AreEqual(2, dict.Count);

			var even = dict["even"];
			Assert.IsNotNull(even);
			Assert.AreEqual(4, even.Count);
			Assert.IsTrue(even.Contains(2));
			Assert.IsTrue(even.Contains(4));
			Assert.IsTrue(even.Contains(6));
			Assert.IsTrue(even.Contains(8));

			var odd = dict["odd"];
			Assert.IsNotNull(odd);
			Assert.AreEqual(5, odd.Count);
			Assert.IsTrue(odd.Contains(1));
			Assert.IsTrue(odd.Contains(3));
			Assert.IsTrue(odd.Contains(5));
			Assert.IsTrue(odd.Contains(7));
			Assert.IsTrue(odd.Contains(9));
		}

		[TestMethod]
		public void TestInsertRange()
		{
			var tmp = new List<uint>(32);
			var tmpArr = new uint[32];

			for (int run = 0; run < 10000; run++)
			{
				var cap = PRNG.Next(2, 7);
				var fast = new FastList<uint>(cap);
				var list = new List<uint>(cap);

				var numOps = PRNG.Next(2, 4);
				for (int o = 0; o < numOps; o++)
				{
					tmp.Clear();
					var tmpLen = PRNG.Next(1, 9);
					for (int n = 0; n < tmpLen; n++)
						tmp.Add(PRNG.NextUInt32());

					var insIdx = list.Count == 0 ? 0 : PRNG.Next(0, list.Count);

					// go!
					tmp.CopyTo(tmpArr);
					fast.InsertRange(insIdx, tmpArr.AsSpan(0, tmpLen));
					list.InsertRange(insIdx, tmp);

					// verify
					Assert.AreEqual(list.Count, fast.Count);
					for (int v = 0; v < list.Count; v++)
						Assert.AreEqual(list[v], fast[v]);
				}
			}
		}
	}
}


<|start_filename|>tests/TokenizerTests.cs<|end_filename|>
﻿using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;

using Lidgren.Core;

namespace UnitTests
{
	[TestClass]
	public class TokenizerTests
	{
		[TestMethod]
		public void TestTokenizer()
		{
			TestSplit("hello this is a string", ' ');
			TestSplit("hello this is a string", '%');
			TestSplit("hello   this   is   a   string", ' ');
			TestSplit("  hello this is a string  ", ' ');
			TestSplit("    ", ' ');


			var str = "split-on,multiple things";
			var t = new Tokenizer<char>(str);

			var ok = t.GetNext("-, ".AsSpan(), out var token);
			Assert.IsTrue(ok);
			Assert.IsTrue(token.SequenceEqual("split"));

			ok = t.GetNext("-, ".AsSpan(), out token);
			Assert.IsTrue(ok);
			Assert.IsTrue(token.SequenceEqual("on"));

			ok = t.GetNext("-, ".AsSpan(), out token);
			Assert.IsTrue(ok);
			Assert.IsTrue(token.SequenceEqual("multiple"));

			ok = t.GetNext("-, ".AsSpan(), out token);
			Assert.IsTrue(ok);
			Assert.IsTrue(token.SequenceEqual("things"));

			ok = t.GetNext("-, ".AsSpan(), out token);
			Assert.IsFalse(ok);
		}

		private void TestSplit(string str, char delimiter)
		{
			var facit = str.Split(' ');

			var tok = new Tokenizer<char>(str);
			for (int i = 0; i < facit.Length; i++)
				Assert.IsTrue(tok.Next(' ').SequenceEqual(facit[i]));
			Assert.IsTrue(tok.Next(' ') == default);

			Span<Range> tokens = stackalloc Range[32];
			int numTokens = Tokenizer<char>.Split(str, ' ', tokens);
			Assert.AreEqual(facit.Length, numTokens);

			for (int i = 0; i < facit.Length; i++)
			{
				(var off, var len) = tokens[i].GetOffsetAndLength(str.Length);
				Assert.IsTrue(str.AsSpan(off, len).SequenceEqual(facit[i]));
			}
		}
	}
}


<|start_filename|>tests/JobServiceTests.cs<|end_filename|>
﻿using System;
using System.Threading;
using Lidgren.Core;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace UnitTests
{
	[TestClass]
	public class JobServiceTests
	{
		private double m_delayEnded, m_delayStarted;

		[TestMethod]
		public void TestJobService()
		{
			JobService.Initialize();

			//TimingService.IsEnabled = true;
			//var chrome = new ChromeTraceTimingConsumer("jobtests.json");

			Console.WriteLine($"JobService running with {JobService.WorkersCount.ToString()} workers");

			var action = new Action<object>(Work);
			var argument = "hello";

			var anotherAction = new Action<object>(MoreWork);
			var anotherArgument = "goodbye";

			var actionsList = new Action<object>[] { Work, MoreWork, Work, MoreWork };
			var argumentsList = new object[] { "hi", "howdy", "yello", "wassup" };

			// schedule a single job to run; returns immediately
			JobService.Enqueue((x) => { Console.WriteLine("hello from job"); });

			m_delayStarted = TimeService.Wall;
			m_delayEnded = 0;
			JobService.Enqueue("delayed", (x) =>
			{
				m_delayEnded = TimeService.Wall;
			}, null, 1.8);

			// runs action(argument) on a worker thread; returns immediately
			JobService.Enqueue("singleFF", action, argument);

			// runs action(argument) on a worker thread; calls anotherAction(anotherArgument) when it is complete; returns immediately
			JobService.Enqueue("singleWc", action, argument, anotherAction, anotherArgument);

			// schedule a wide job; blocks until all has completed
			JobService.EnqueueWideBlock("wideBlk", action, argument);

			// schedule a wide job; call 'anotherAction' when all has finished; returns immediately
			JobService.EnqueueWide("wideWc", action, argument, anotherAction, anotherArgument);

			// schedule a wide job; returns immediately
			JobService.EnqueueWide("wideFF", action, argument, null, null);

			// run each action in list concurrently with the argument; blocks until all has completed
			JobService.ForEachActionBlock("feActionBlk", actionsList, argument);

			// run each action in list concurrently with the argument; when all completed call anotherAction(anotherArgument); returns immediately
			JobService.ForEachAction("feActionWc", actionsList, argument, anotherAction, anotherArgument);

			// schedule work to be done on each argument in list, return immediately
			JobService.ForEachAction("feActionFF", actionsList, argument, null, null);

			// schedule work to be done on each argument in list; blocks until all has completed
			JobService.ForEachArgumentBlock<object>("feArgBlk", action, argumentsList);

			// schedule work to be done on each argument in list; when all completed call anotherAction(anotherArgument); returns immediately
			JobService.ForEachArgument("feArgWc", action, argumentsList, anotherAction, anotherArgument);

			// schedule work to be done on each argument in list; returns immediately
			JobService.ForEachArgument("feArgFF", action, argumentsList, null, null);

			Thread.Sleep(2250);

			double delay = m_delayEnded - m_delayStarted;
			Assert.IsTrue(delay > 1.8 && delay < 1.9, $"Delay is {delay} should be >1.8");

			//TimingService.TriggerFlush(true); // this will set TimingService.IsEnabled to false
			//chrome.Dispose();

			JobService.Shutdown();
		}

		public static void Work(object ob)
		{
			Console.WriteLine("Work(" + ob.ToString() + ")");
			Thread.Sleep(PRNG.Next(2, 5));
		}

		public static void MoreWork(object ob)
		{
			Console.WriteLine("MoreWork(" + ob.ToString() + ")");
			Thread.Sleep(PRNG.Next(2, 5));
		}
	}
}


<|start_filename|>src/Collections/FastList/FastList.Add.cs<|end_filename|>
﻿using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;

namespace Lidgren.Core
{
	public partial class FastList<T>
	{
		// cold path, never inline
		[MethodImpl(MethodImplOptions.NoInlining)]
		private T[] Grow(int minAddCount)
		{
			// Don't grow by doubling; only by +50% of current
			// Downside: need to trust user to call EnsureCapacity and use initialCapacity wisely for good perf
			// Upside: less memory waste
			int bufLen = m_buffer.Length;
			int newSize = 4 + Math.Max(bufLen + bufLen / 2, m_count + minAddCount);
			var old = this.ReadOnlySpan;
			var newBuffer = new T[newSize];
			old.CopyTo(newBuffer);
			m_offset = 0;
			m_buffer = newBuffer;
			return newBuffer;
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void EnsureCapacity(int toAddItemsCount)
		{
			if (m_count + toAddItemsCount > m_buffer.Length)
				Grow(toAddItemsCount);
		}

		// cold path, never inline
		[MethodImpl(MethodImplOptions.NoInlining)]
		private T[] Grow()
		{
			var oldLen = m_buffer.Length;
			var newLength = oldLen == 0 ? 4 : oldLen * 2;
			var newBuffer = new T[newLength];

			var old = this.ReadOnlySpan;
			old.CopyTo(newBuffer);

			m_buffer = newBuffer;
			m_offset = 0;
			return newBuffer;
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void Add(in T item)
		{
			ref var loc = ref AddUninitialized();
			loc = item;
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public ref T Add()
		{
			ref var loc = ref AddUninitialized();
			loc = default(T);
			return ref loc;
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public ref T AddUninitialized()
		{
			int count = m_count;
			var buffer = m_buffer;
			if (count == buffer.Length)
				buffer = Grow();
			int idx = m_offset + count;
			if (idx >= buffer.Length)
			{
				Compact();
				idx = count;
			}
			m_count = count + 1;
			return ref buffer[idx];
		}

		/// <summary>
		/// Reserves items at end of list and returns span
		/// </summary>
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public Span<T> AddRange(int numItems)
		{
			var span = AddRangeUninitialized(numItems);
			span.Clear();
			return span;
		}

		/// <summary>
		/// Reserves items at end of list and returns span; may contain uninitialized values
		/// </summary>
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public Span<T> AddRangeUninitialized(int numItems)
		{
			var buffer = m_buffer;
			int cnt = m_count;
			if (cnt + numItems >= buffer.Length)
				buffer = Grow(numItems);
			if (m_offset + cnt + numItems > buffer.Length)
				Compact();
			var retval = new Span<T>(buffer, m_offset + cnt, numItems);
			m_count = cnt + numItems;
			return retval;
		}

		/// <summary>
		/// Adds items to end of list
		/// </summary>
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void AddRange(ReadOnlySpan<T> addItems)
		{
			var span = AddRangeUninitialized(addItems.Length);
			addItems.CopyTo(span);
		}

		/// <summary>
		/// Adds items to end of list; using IEnumerable (much slower than adding spans)
		/// </summary>
		public void AddRangeSlow(IEnumerable<T> addItems)
		{
			foreach (var item in addItems)
				Add(item);
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void Insert(int index, in T value)
		{
			ref var destination = ref InsertUninitialized(index);
			destination = value;
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public ref T InsertUninitialized(int index)
		{
			int count = m_count;
			CoreException.Assert(index <= count);
			var buffer = m_buffer;
			if (count >= buffer.Length)
				buffer = Grow();
			var offset = m_offset;
			if (offset > 0)
			{
				if (index == 0)
				{
					// yay
					m_count = count + 1;
					offset = offset - 1;
					m_offset = offset;
					return ref buffer[offset];
				}

				if (offset + count == buffer.Length)
				{
					// need to compact
					Compact();
					offset = 0;
				}
			}

			int bufferIndex = offset + index;

			int copyLen = count - index;
			if (copyLen > 0)
			{
				// push forward
				var cache = buffer[bufferIndex];
				for (int i = 0; i < copyLen; i++)
				{
					// swap tmp and destination
					int dstBufIdx = bufferIndex + i + 1;
					var tmp = buffer[dstBufIdx];
					buffer[dstBufIdx] = cache;
					cache = tmp;
				}
			}
			m_count = count + 1;
			return ref m_buffer[bufferIndex];
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public ref T Insert(int index)
		{
			ref T destination = ref InsertUninitialized(index);
			destination = default;
			return ref destination;
		}

		public void InsertRange(int index, ReadOnlySpan<T> items)
		{
			var buf = m_buffer;
			var count = m_count;
			if (count + items.Length > buf.Length)
			{
				// need to allocate memory to fit
				InsertRange_Allocate(index, items);
				return;
			}

			if (m_offset + count + items.Length > buf.Length)
				Compact(); // need to compact to fit

			// ok, we should fit in current size/offset

			// push tail
			var tailLen = count - index;
			if (tailLen > 0)
			{
				var tail = buf.AsSpan(m_offset + index, tailLen);
				var tailDst = buf.AsSpan(m_offset + index + items.Length, tailLen);
				tail.CopyTo(tailDst);
			}

			// insert
			items.CopyTo(buf.AsSpan(m_offset + index, items.Length));
			m_count = count + items.Length;
		}

		// InsertRange() when we need to reallocate entire array
		private void InsertRange_Allocate(int index, ReadOnlySpan<T> items)
		{
			var oldBuffer = m_buffer;
			var count = m_count;
			int oldLen = oldBuffer.Length;
			int newSize = 4 + Math.Max(oldLen + oldLen / 2, count + items.Length);
			var newBuffer = new T[newSize];

			// pre
			if (index > 0)
			{
				var pre = oldBuffer.AsSpan(m_offset, index);
				pre.CopyTo(newBuffer.AsSpan(0, index));
			}

			// insert
			items.CopyTo(newBuffer.AsSpan(index, items.Length));

			// tail
			var tailLen = count - index;
			if (tailLen > 0)
			{
				var tail = oldBuffer.AsSpan(m_offset + index, tailLen);
				tail.CopyTo(newBuffer.AsSpan(index + items.Length, tailLen));
			}

			m_buffer = newBuffer;
			m_offset = 0;
			m_count = count + items.Length;
		}

		/// <summary>
		/// Adds 'numItems' copies of 'item' to list
		/// </summary>
		public void Fill(int numItems, in T item)
		{
			var span = AddRangeUninitialized(numItems);
			span.Fill(item);
		}
	}
}


<|start_filename|>src/JobService/Job.cs<|end_filename|>
﻿using System;

namespace Lidgren.Core
{
	internal struct Job
	{
		public string Name;
		public Action<object> Work;
		public object Argument;
		public JobCompletion Completion;
	}
}


<|start_filename|>src/Hash/HashUtil.cs<|end_filename|>
﻿#nullable enable
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

namespace Lidgren.Core
{
	public static class HashUtil
	{
		public static uint Hash32(ReadOnlySpan<byte> bytes)
		{
			var hasher = Hasher.Create();
			hasher.Add(bytes);
			return hasher.Finalize32();
		}

		public static ulong Hash64(ReadOnlySpan<byte> bytes)
		{
			var hasher = Hasher.Create();
			hasher.Add(bytes);
			return hasher.Finalize64();
		}

		public static uint Hash32(ReadOnlySpan<char> str)
		{
			var hasher = Hasher.Create();
			hasher.Add(str);
			return hasher.Finalize32();
		}

		public static ulong Hash64(ReadOnlySpan<char> str)
		{
			var hasher = Hasher.Create();
			hasher.Add(str);
			return hasher.Finalize64();
		}

		/// <summary>
		/// Hash string converted to utf-8; note: stackallocs 2x length of string
		/// </summary>
		public static ulong Utf8Hash64(ReadOnlySpan<char> str)
		{
			Span<byte> bytes = stackalloc byte[str.Length * 2];
			int cnt = System.Text.Encoding.UTF8.GetBytes(str, bytes);
			return Hash64(bytes.Slice(0, cnt));
		}

		/// <summary>
		/// Hash string converted to utf-8; note: stackallocs 2x length of string
		/// </summary>
		public static uint Utf8Hash32(ReadOnlySpan<char> str)
		{
			Span<byte> bytes = stackalloc byte[str.Length * 2];
			int cnt = System.Text.Encoding.UTF8.GetBytes(str, bytes);
			return Hash32(bytes.Slice(0, cnt));
		}

		public static uint HashLower32(ReadOnlySpan<char> str)
		{
			var hasher = Hasher.Create();
			hasher.AddLowerInvariant(str);
			return hasher.Finalize32();
		}

		public static ulong HashLower64(ReadOnlySpan<char> str)
		{
			var hasher = Hasher.Create();
			hasher.AddLowerInvariant(str);
			return hasher.Finalize64();
		}

		public static ulong Hash64<T>(ref T instance, int byteCount) where T : struct
		{
			var bytes = MemoryMarshal.AsBytes(MemoryMarshal.CreateSpan<T>(ref instance, 1));
			var hasher = Hasher.Create();
			hasher.Add(bytes);
			return hasher.Finalize64();
		}

		public static ulong Hash64(ulong value)
		{
			// MurmurHash3 finalizer
			value ^= value >> 33;
			value *= 0xff51afd7ed558ccd;
			value ^= value >> 33;
			value *= 0xc4ceb9fe1a85ec53;
			value ^= value >> 33;
			return value;
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static ulong Hash64(float x, float y)
		{
			return Hash64(TwoSinglesULongUnion.ReinterpretCast(x, y));
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static ulong Hash64(float x, float y, float z)
		{
			var hasher = Hasher.Create();
			hasher.AddAligned64(TwoSinglesULongUnion.ReinterpretCast(x, y));
			hasher.Add(SingleUIntUnion.ReinterpretCast(z));
			return hasher.Finalize64();
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static ulong Hash64(float x, float y, float z, float w)
		{
			var hasher = Hasher.Create();
			hasher.AddAligned64(TwoSinglesULongUnion.ReinterpretCast(x, y));
			hasher.AddAligned64(TwoSinglesULongUnion.ReinterpretCast(z, w));
			return hasher.Finalize64();
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static uint Hash32(float x, float y)
		{
			return HashUtil.XorFold64To32(Hash64(x, y));
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static uint Hash32(float x, float y, float z)
		{
			var hasher = Hasher.Create();
			hasher.AddAligned64(TwoSinglesULongUnion.ReinterpretCast(x, y));
			hasher.Add(SingleUIntUnion.ReinterpretCast(z));
			return hasher.Finalize32();
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static uint Hash32(float x, float y, float z, float w)
		{
			var hasher = Hasher.Create();
			hasher.AddAligned64(TwoSinglesULongUnion.ReinterpretCast(x, y));
			hasher.AddAligned64(TwoSinglesULongUnion.ReinterpretCast(z, w));
			return hasher.Finalize32();
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static uint Hash32(ulong one, ulong two)
		{
			return HashUtil.XorFold64To32(Hash64(one, two));
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static ulong Hash64(ulong one, ulong two)
		{
			// inlined partial murmur hash
			const ulong M = 0xc6a4a7935bd1e995ul;
			ulong hash = M;

			one *= M;
			one ^= one >> 47;
			one *= M;
			hash ^= one;
			hash *= M;

			two *= M;
			two ^= two >> 47;
			two *= M;
			hash ^= two;
			hash *= M;

			// finalize
			hash ^= hash >> 47;
			hash *= M;
			hash ^= hash >> 47;

			return hash;
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static uint XorFold64To32(ulong value)
		{
			unchecked
			{
				return (uint)value ^ (uint)(value >> 32);
			}
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static ushort XorFold64To16(ulong value)
		{
			return XorFold32To16(XorFold64To32(value));
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static ushort XorFold32To16(uint value)
		{
			unchecked
			{
				return (ushort)(value ^ (value >> 16));
			}
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static byte XorFold32To8(uint value)
		{
			unchecked
			{
				return (byte)(value ^ (value >> 8) ^ (value >> 16) ^ (value >> 24));
			}
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static ushort XorFold16To8(ushort value)
		{
			unchecked
			{
				return (byte)(value ^ (byte)(value >> 8));
			}
		}

		// weyl constants
		private const uint c_WEYLW0 = 0x3504f333u;   // 3*2309*128413 
		private const uint c_WEYLW1 = 0xf1bbcdcbu;   // 7*349*1660097 
		private const uint c_WEYLM = 741103597u;    // 13*83*686843

		/// <summary>
		/// Weyl hash
		/// </summary>
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static uint Hash32(uint x, uint y)
		{
			x *= c_WEYLW0;
			y *= c_WEYLW1;
			x ^= y;
			x *= c_WEYLM;
			return x;
		}

		public static uint DJBa2(ReadOnlySpan<byte> bytes)
		{
			uint hash = 5381u;
			for (int i = 0; i < bytes.Length; i++)
				hash = ((hash << 5) + hash) ^ bytes[i];
			return hash;
		}

		public static uint FNV1aLower(ReadOnlySpan<char> str)
		{
			unchecked
			{
				uint retval = 2166136261u;
				for (int i = 0; i < str.Length; i++)
					retval = (retval ^ (uint)char.ToLower(str[i])) * 16777619u;
				return retval;
			}
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static uint FNV1a(ReadOnlySpan<char> str)
		{
			unchecked
			{
				uint retval = 2166136261u;
				for (int i = 0; i < str.Length; i++)
					retval = (retval ^ (uint)str[i]) * 16777619u;
				return retval;
			}
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static ulong FNV1a64(ReadOnlySpan<byte> data)
		{
			unchecked
			{
				ulong retval = 14695981039346656037ul;
				for (int i = 0; i < data.Length; i++)
					retval = (retval ^ data[i]) * 1099511628211ul;
				return retval;
			}
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static ulong FNV1a64(ReadOnlySpan<char> str)
		{
			unchecked
			{
				ulong retval = 14695981039346656037ul;
				for (int i = 0; i < str.Length; i++)
					retval = (retval ^ (uint)str[i]) * 1099511628211ul;
				return retval;
			}
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static ulong FNV1a64Lower(ReadOnlySpan<char> str)
		{
			unchecked
			{
				ulong retval = 14695981039346656037ul;
				for (int i = 0; i < str.Length; i++)
					retval = (retval ^ (uint)char.ToLower(str[i])) * 1099511628211ul;
				return retval;
			}
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static uint FNV1a(ReadOnlySpan<byte> data)
		{
			unchecked
			{
				uint retval = 2166136261u;
				for (int i = 0; i < data.Length; i++)
					retval = (retval ^ (uint)data[i]) * 16777619u;
				return retval;
			}
		}
	}
}


<|start_filename|>src/Collections/SymmetricMatrixBool.cs<|end_filename|>
﻿using System;
using System.Runtime.CompilerServices;

namespace Lidgren.Core
{
	/// <summary>
	/// Special case SymmetrixMatrix of bool where each boolean is stored as a single bit
	/// </summary>
	public class SymmetricMatrixBool
	{
		private ulong[] m_data;

		public SymmetricMatrixBool(int size)
		{
			var len = ((size * size) + size) / 2;
			int units = 1 + ((len - 1) / 64);
			m_data = new ulong[units];
		}

		public void Clear()
		{
			m_data.AsSpan().Clear();
		}

		public bool this[int x, int y]
		{
			[MethodImpl(MethodImplOptions.AggressiveInlining)]
			get
			{
				var index = GetIndex(x, y);
				return (m_data[index >> 6] & (1ul << (index & 63))) != 0ul;
			}
			[MethodImpl(MethodImplOptions.AggressiveInlining)]
			set
			{
				var index = GetIndex(x, y);
				if (value)
					m_data[index >> 6] |= (1ul << (index & 63));
				else
					m_data[index >> 6] &= ~(1ul << (index & 63));
			}
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		private static int GetIndex(int x, int y)
		{
			if (y > x)
				return (((y * y) + y) / 2) + x;
			else
				return (((x * x) + x) / 2) + y;
		}
	}
}


<|start_filename|>tests/ColorTests.cs<|end_filename|>
﻿using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;

using Lidgren.Core;

namespace UnitTests
{
	[TestClass]
	public class ColorTests
	{
		[TestMethod]
		public void TestColor()
		{
			var col = new Color((byte)1, (byte)2, (byte)3, (byte)4);
			Assert.AreEqual(1, col.R);
			Assert.AreEqual(2, col.G);
			Assert.AreEqual(3, col.B);
			Assert.AreEqual(4, col.A);

			col.GetRGBA(out var red, out var green, out var blue, out var alpha);
			Assert.AreEqual(1, red);
			Assert.AreEqual(2, green);
			Assert.AreEqual(3, blue);
			Assert.AreEqual(4, alpha);

			var rt = new Color(col.RGBA);
			Assert.AreEqual(rt.RGBA, col.RGBA);
			Assert.AreEqual(rt, col);

			var hex = col.ToHex();
			Assert.AreEqual("01020304", hex);

			col = Color.FromHex("#FF881177");
			Assert.AreEqual(0xFF, col.R);
			Assert.AreEqual(0x88, col.G);
			Assert.AreEqual(0x11, col.B);
			Assert.AreEqual(0x77, col.A);

			Color.Random(0.5f);

			var c1 = Color.FromHex("#FFA50033");
			var c2 = new Color(255, 165, 0, 51);
			Assert.AreEqual(c1, c2);

			var redcol = Color.Red;
			Assert.AreEqual(255, redcol.R);
			Assert.AreEqual(0, redcol.G);
			Assert.AreEqual(0, redcol.B);
			Assert.AreEqual(255, redcol.A);
		}
	}
}


<|start_filename|>src/ProcessUtil.cs<|end_filename|>
﻿using System;
using System.Diagnostics;
using System.IO;
using System.Text;

namespace Lidgren.Core
{
    public static class ProcessUtil
    {
		public enum RunProcessResult
		{
			FailedToStartProcess,
			TimedOut,
			ExitCodeZero,
			ExitCodeNonZero
		}

		/// <summary>
		/// Run shell command; f.ex. for opening html links. Do not block, return immediately
		/// </summary>
		public static Process RunShell(string command, string arguments)
		{
			var process = new Process();
			process.StartInfo.Verb = command;
			process.StartInfo.Arguments = arguments;
			process.StartInfo.UseShellExecute = true;
			process.Start();
			return process;
		}

		/// <summary>
		/// Run shell command; f.ex. for opening html links; blocks until complete or timeout and returns error code
		/// </summary>
		public static RunProcessResult RunShell(string command, string arguments, out int exitCode, TimeSpan timeout)
		{
			var process = RunShell(command, arguments);
			bool exited = process.WaitForExit((int)timeout.TotalMilliseconds);
			if (!exited)
			{
				exitCode = -1;
				return RunProcessResult.TimedOut;
			}

			exitCode = process.ExitCode;
			if (exitCode == 0)
				return RunProcessResult.ExitCodeZero;
			return RunProcessResult.ExitCodeNonZero;
		}

		/// <summary>
		/// Do not use shell execute and redirect output; blocks until complete or timeout and returns error code
		/// </summary>
		public static RunProcessResult RunBlocking(
			string executable,
			string arguments,
			string workingDirectory,
			TimeSpan timeout,
			out int exitCode,
			out string stdOut,
			out string stdErr)
		{
			stdOut = "";
			stdErr = "";
			exitCode = -1;

			var process = new Process();
			process.StartInfo.FileName = executable;
			process.StartInfo.Arguments = arguments;
			process.StartInfo.CreateNoWindow = true;
			process.StartInfo.UseShellExecute = false;

			var stdOutBdr = new StringBuilder();
			process.StartInfo.RedirectStandardOutput = true;
			process.OutputDataReceived += (sender, data) => {
				stdOutBdr.AppendLine(data.Data);
			};

			var stdErrBdr = new StringBuilder();
			process.StartInfo.RedirectStandardError = true;
			process.ErrorDataReceived += (sender, data) => {
				stdErrBdr.AppendLine(data.Data);
			};

			bool ok = process.Start();
			if (!ok)
				return RunProcessResult.FailedToStartProcess;

			// read output
			process.BeginOutputReadLine();
			process.BeginErrorReadLine();

			bool exited = process.WaitForExit((int)timeout.TotalMilliseconds);
			if (!exited)
				return RunProcessResult.TimedOut;

			stdOut = stdOutBdr.ToString();
			stdErr = stdErrBdr.ToString();
			exitCode = process.ExitCode;

			if (exitCode == 0)
				return RunProcessResult.ExitCodeZero;
			return RunProcessResult.ExitCodeNonZero;
		}
	}
}


<|start_filename|>tests/DataWriterTests.cs<|end_filename|>
﻿using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;

using Lidgren.Core;
using System.Numerics;

namespace UnitTests
{
	[TestClass]
	public class DataWriterTests
	{
		[TestMethod]
		public void TestDataWriter()
		{
			using (var wrt = new DataWriter(3))
			{
				wrt.WriteBool(true);
				wrt.WriteDouble(1.88);
				wrt.WriteVariableInt32(12345);
				wrt.WriteLengthPrefixedArray<ushort>(new ushort[] { 12, 1, 65000 });
				wrt.Write<Vector3>(new Vector3(9, 91, 81));
				wrt.WriteString("Pararibulitis");
				wrt.WriteVariableUInt64(0ul);
				wrt.WriteVector3(new Vector3(1, 2, 3));
				wrt.WriteVector4(new Vector4(1, 2, 3, 4));

				var rdr = wrt.ReadOnlySpan;

				Assert.IsTrue(rdr.ReadBool());
				Assert.AreEqual(1.88, rdr.ReadDouble());
				Assert.AreEqual(12345, rdr.ReadVariableInt64());

				var arr = rdr.ReadLengthPrefixedArray<ushort>();
				Assert.AreEqual(3, arr.Length);
				Assert.AreEqual(12, arr[0]);
				Assert.AreEqual(1, arr[1]);
				Assert.AreEqual(65000, arr[2]);

				var v = rdr.Read<Vector3>();
				Assert.AreEqual(new Vector3(9, 91, 81), v);
				Assert.AreEqual("Pararibulitis", rdr.ReadString());
				Assert.AreEqual(0ul, rdr.ReadVariableUInt64());
				Assert.AreEqual(new Vector3(1, 2, 3), rdr.ReadVector3());
				Assert.AreEqual(new Vector4(1, 2, 3, 4), rdr.ReadVector4());
			}
		}

		[TestMethod]
		public void TestDataWriter2()
		{
			ulong ss = RandomSeed.GetUInt64();

			byte[] small = new byte[64];

			using (var wrt = new DataWriter(1))
			{
				for (int runs = 0; runs < 10000; runs++)
				{
					wrt.Clear();
					var originalSeed = PRNG.NextUInt64(ref ss);

					var seed = originalSeed;
					{
						int numOps = PRNG.Next(ref seed, 1, 15);
						for (int o = 0; o < numOps; o++)
						{
							switch (PRNG.Next(ref seed, 0, 6))
							{
								case 0:
									wrt.WriteBool(PRNG.NextBool(ref seed));
									break;
								case 1:
									wrt.WriteSingle((float)PRNG.Next(ref seed, 1, 100));
									break;
								case 2:
									wrt.WriteVariableInt32(PRNG.Next(ref seed, -50, 50));
									break;
								case 3:
									wrt.WriteString(PRNG.NextUInt32(ref seed).ToString());
									break;
								case 4:
									wrt.WriteUInt16((ushort)PRNG.Next(ref seed, 0, ushort.MaxValue));
									break;
								case 5:
									var sss = small.AsSpan(0, PRNG.Next(ref seed, 1, 25));
									wrt.WriteBytes(sss);
									break;
							}
						}
					}

					// read
					var rdr = wrt.ReadOnlySpan;

					seed = originalSeed;
					{
						int numOps = PRNG.Next(ref seed, 1, 15);
						for (int o = 0; o < numOps; o++)
						{
							switch (PRNG.Next(ref seed, 0, 6))
							{
								case 0:
									{
										var facit = PRNG.NextBool(ref seed);
										Assert.AreEqual(facit, rdr.ReadBool());
									}
									break;
								case 1:
									{
										var facit = (float)PRNG.Next(ref seed, 1, 100);
										Assert.AreEqual(facit, rdr.ReadSingle());
									}
									break;
								case 2:
									{
										var facit = PRNG.Next(ref seed, -50, 50);
										Assert.AreEqual(facit, rdr.ReadVariableInt32());
									}
									break;
								case 3:
									{
										var facit = PRNG.NextUInt32(ref seed).ToString();
										Assert.AreEqual(facit, rdr.ReadString());
									}
									break;
								case 4:
									{
										var facit = (ushort)PRNG.Next(ref seed, 0, ushort.MaxValue);
										Assert.AreEqual(facit, rdr.ReadUInt16());
									}
									break;
								case 5:
									{
										var sss = small.AsSpan(0, PRNG.Next(ref seed, 1, 25));
										var aaa = rdr.ReadBytes(sss.Length);
										Assert.IsTrue(sss.SequenceEqual(aaa));
									}
									break;
							}
						}
					}
				}
			}
		}
	}
}


<|start_filename|>tests/WildcardMatcherTests.cs<|end_filename|>
﻿using System;
using Lidgren.Core;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace UnitTests
{
	[TestClass]
	public partial class WildcardMatcherTests
	{
		[TestMethod]
		public void TestWildcards()
		{
			var wc = new WildcardMatcher("ab*");
			Assert.IsTrue(wc.Matches("ab"));
			Assert.IsTrue(wc.Matches("abc"));
			Assert.IsTrue(wc.Matches("abcdefgh"));
			Assert.IsFalse(wc.Matches("ac"));
			Assert.IsFalse(wc.Matches("acb"));
			Assert.IsFalse(wc.Matches("xab"));
			Assert.IsFalse(wc.Matches("xabx"));
			Assert.IsFalse(wc.Matches(""));

			wc = new WildcardMatcher("*ab");
			Assert.IsTrue(wc.Matches("ab"));
			Assert.IsTrue(wc.Matches("xab"));
			Assert.IsTrue(wc.Matches("xyzab"));
			Assert.IsFalse(wc.Matches("abx"));
			Assert.IsFalse(wc.Matches("xxabxx"));
			Assert.IsFalse(wc.Matches(""));

			wc = new WildcardMatcher("a*b");
			Assert.IsTrue(wc.Matches("ab"));
			Assert.IsTrue(wc.Matches("axb"));
			Assert.IsTrue(wc.Matches("aaabb"));

			Assert.IsFalse(wc.Matches(""));
			Assert.IsFalse(wc.Matches("a"));
			Assert.IsFalse(wc.Matches("b"));
			Assert.IsFalse(wc.Matches("yab"));
			Assert.IsFalse(wc.Matches("yaxb"));
			Assert.IsFalse(wc.Matches("yaaabb"));
			Assert.IsFalse(wc.Matches("aby"));
			Assert.IsFalse(wc.Matches("axby"));
			Assert.IsFalse(wc.Matches("aaabby"));

			wc = new WildcardMatcher("years?ago");
			Assert.IsTrue(wc.Matches("years1ago"));
			Assert.IsTrue(wc.Matches("yearsXago"));
			Assert.IsTrue(wc.Matches("years?ago"));
			Assert.IsTrue(wc.Matches("yearsaago"));
			Assert.IsTrue(wc.Matches("yearssago"));
			Assert.IsFalse(wc.Matches(""));
			Assert.IsFalse(wc.Matches("yearsago"));
			Assert.IsFalse(wc.Matches("yearsXXXago"));
			Assert.IsFalse(wc.Matches("yearsago"));
			Assert.IsFalse(wc.Matches(null));
			Assert.IsFalse(wc.Matches("yyyyyXago"));

			wc = new WildcardMatcher("*hello*");
			Assert.IsTrue(wc.Matches("1hello2"));
			Assert.IsTrue(wc.Matches("asdfhelloasdf"));
			Assert.IsTrue(wc.Matches("helloXxx"));
			Assert.IsTrue(wc.Matches("Xxxhello"));
			Assert.IsTrue(wc.Matches("hello"));
			Assert.IsFalse(wc.Matches("hell"));
			Assert.IsFalse(wc.Matches("hellXo"));
			Assert.IsFalse(wc.Matches("111hellXo111"));
			Assert.IsFalse(wc.Matches("111hell111o"));

			// The tests below are borrowed from
			// from https://bitbucket.org/hasullivan/fast-wildcard-matching-testing/src/master/WildCardTesting/Program.cs
			// by <NAME>

			var texts = new FastList<string>(64);
			var filters = new FastList<string>(64);
			var expected = new FastList<bool>(64);

			texts.Add("abcccd");
			filters.Add("*ccd");
			expected.Add(true);

			texts.Add("adacccdcccd");
			filters.Add("*ccd");
			expected.Add(true);

			texts.Add("mississipissippi");
			filters.Add("*issip*ss*");
			expected.Add(true);

			texts.Add("mississipissippi");
			filters.Add("*mi*issip*ss*");
			expected.Add(true);

			texts.Add("xxxxzzzzzzzzyf");
			filters.Add("xxxx*zzy*fffff");
			expected.Add(false);

			texts.Add("xxxxzzzzzzzzyf");
			filters.Add("xxxx*zzy*f");
			expected.Add(true);

			texts.Add("xyxyxyzyxyz");
			filters.Add("xy*z*xyz");
			expected.Add(true);

			texts.Add("mississippi");
			filters.Add("*sip*");
			expected.Add(true);

			texts.Add("xyxyxyxyz");
			filters.Add("xy*xyz");
			expected.Add(true);

			texts.Add("mississippi");
			filters.Add("mi*sip*");
			expected.Add(true);

			texts.Add("ababac");
			filters.Add("*abac*");
			expected.Add(true);

			texts.Add("ababac");
			filters.Add("*abac");
			expected.Add(true);

			texts.Add("aaazz");
			filters.Add("a*zz*");
			expected.Add(true);

			texts.Add("a12b12");
			filters.Add("*12*23");
			expected.Add(false);

			texts.Add("a12b12");
			filters.Add("a12b");
			expected.Add(false);

			texts.Add("a12b12");
			filters.Add("*12*12*");
			expected.Add(true);

			texts.Add("XYXYXYZYXYz");
			filters.Add("XY*Z*XYz");
			expected.Add(true);

			texts.Add("missisSIPpi");
			filters.Add("*SIP*");
			expected.Add(true);

			texts.Add("mississipPI");
			filters.Add("*issip*PI");
			expected.Add(true);

			texts.Add("xyxyxyxyz");
			filters.Add("xy*xyz");
			expected.Add(true);

			texts.Add("miSsissippi");
			filters.Add("mi*sip*");
			expected.Add(true);

			texts.Add("miSsissippi");
			filters.Add("mi*Sip*");
			expected.Add(false);

			texts.Add("abAbac");
			filters.Add("Abac*");
			expected.Add(false);

			texts.Add("abAbac");
			filters.Add("*Abac*");
			expected.Add(true);

			texts.Add("aAazz");
			filters.Add("a*zz*");
			expected.Add(true);

			texts.Add("A12b12");
			filters.Add("*12*23");
			expected.Add(false);

			texts.Add("a12B12");
			filters.Add("*12*12*");
			expected.Add(true);

			texts.Add("oWn");
			filters.Add("*oWn*");
			expected.Add(true);

			texts.Add("bLah");
			filters.Add("bLah");
			expected.Add(true);

			texts.Add("bLah");
			filters.Add("bLaH");
			expected.Add(false);

			texts.Add("a");
			filters.Add("*?");
			expected.Add(true);

			texts.Add("ab");
			filters.Add("*?");
			expected.Add(true);

			texts.Add("abc");
			filters.Add("*?");
			expected.Add(true);

			texts.Add("a");
			filters.Add("??");
			expected.Add(false);

			texts.Add("ab");
			filters.Add("?*?");
			expected.Add(true);

			texts.Add("ab");
			filters.Add("*?*?*");
			expected.Add(true);

			texts.Add("abc");
			filters.Add("?**?*?");
			expected.Add(true);

			texts.Add("abc");
			filters.Add("?**?*&?");
			expected.Add(false);

			texts.Add("abc");
			filters.Add("***&*");
			expected.Add(false);

			texts.Add("abcd");
			filters.Add("?b*??");
			expected.Add(true);

			texts.Add("abcd");
			filters.Add("?a*??");
			expected.Add(false);

			texts.Add("abcd");
			filters.Add("?**?c?");
			expected.Add(true);

			texts.Add("abcd");
			filters.Add("?**?d?");
			expected.Add(false);

			texts.Add("abcde");
			filters.Add("?*b*?*d*?");
			expected.Add(true);

			texts.Add("bLah");
			filters.Add("bL?h");
			expected.Add(true);

			texts.Add("bLaaa");
			filters.Add("bLa?");
			expected.Add(false);

			texts.Add("bLah");
			filters.Add("bLa?");
			expected.Add(true);

			texts.Add("bLaH");
			filters.Add("?Lah");
			expected.Add(false);

			texts.Add("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab");
			filters.Add("a*a*a*a*a*a*aa*aaa*a*a*b");
			expected.Add(true);

			texts.Add("abababababababababababababababababababaacacacacacacacadaeafagahaiajakalaaaaaaaaaaaaaaaaaffafagaagggagaaaaaaaab");
			filters.Add("*a*b*ba*ca*a*x*aaa*fa*ga*b*");
			expected.Add(false);

			texts.Add("aaabbaabbaab");
			filters.Add("*aabbaa*a*");
			expected.Add(true);

			texts.Add("aaaaaaaaaaaaaaaaa");
			filters.Add("*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*");
			expected.Add(true);

			texts.Add("aaaaaaaaaaaaaaaa");
			filters.Add("*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*a*");
			expected.Add(false);

			texts.Add("abc");
			filters.Add("********a********b********c********");
			expected.Add(true);

			texts.Add("abc");
			filters.Add("********a********b********b********");
			expected.Add(false);

			texts.Add("abulomania");
			filters.Add("abulomania");
			expected.Add(true);

			texts.Add("accidentiality");
			filters.Add("accidentiality");
			expected.Add(true);

			texts.Add("aimworthiness");
			filters.Add("aimworthiness");
			expected.Add(true);

			texts.Add("bardocucullus");
			filters.Add("bardocucullus");
			expected.Add(true);

			texts.Add("beaned");
			filters.Add("beaned");
			expected.Add(true);

			texts.Add("bedrowse");
			filters.Add("bedrowse");
			expected.Add(true);

			texts.Add("benumbedness");
			filters.Add("benumbedness");
			expected.Add(true);

			texts.Add("booklists");
			filters.Add("booklists");
			expected.Add(true);

			texts.Add("bratchet");
			filters.Add("bratchet");
			expected.Add(true);

			texts.Add("accidentiality");
			filters.Add("accidentialities");
			expected.Add(false);

			texts.Add("aimworthiness");
			filters.Add("aimworthaness");
			expected.Add(false);

			texts.Add("dardocucullus");
			filters.Add("bardocucullus");
			expected.Add(false);

			texts.Add("beaned");
			filters.Add("beened");
			expected.Add(false);

			texts.Add("bedrowser");
			filters.Add("bedrowsen");
			expected.Add(false);

			texts.Add("benumbedness");
			filters.Add("benumbedniss");
			expected.Add(false);

			texts.Add("booklists");
			filters.Add("booklests");
			expected.Add(false);

			texts.Add("brawtcet");
			filters.Add("bratchet");
			expected.Add(false);

			texts.Add("");
			filters.Add("*");
			expected.Add(true);

			texts.Add("");
			filters.Add("?");
			expected.Add(false);

			texts.Add("Bananas");
			filters.Add("Ba*na*s");
			expected.Add(true);

			texts.Add("Something");
			filters.Add("S*eth??g");
			expected.Add(true);

			texts.Add("Something");
			filters.Add("*");
			expected.Add(true);

			texts.Add("A very long long long stringggggggg");
			filters.Add("A *?string*");
			expected.Add(true);

			texts.Add("Reg: Performance issue when using WebSphere MQ 7.1, Window server 2008 R2 and java 1.6.0_21");
			filters.Add("Reg: Performance issue when using *, Window server ???? R? and java *.*.*_*");
			expected.Add(true);

			texts.Add("Reg: Performance issue when using WebSphere MQ 7.1, Window server 2008 R2 and java 1.6.0_21");
			filters.Add("Reg: Performance* and java 1.6.0_21");
			expected.Add(true);

			texts.Add("Reg: Performance issue when using WebSphere MQ 7.1, Window server 2008 R2 and java 1.6.0_21");
			filters.Add("Reg: Performance issue when using *, Window server ???? R? and java *.*.*_");
			expected.Add(false);

			texts.Add("http://hasullivan.com/");
			filters.Add("*//hasullivan*");
			expected.Add(true);

			texts.Add("http://hasullivan.com/");
			filters.Add("*s*//hasullivan*");
			expected.Add(false);

			texts.Add("http://hasullivan.com/wp-content/uploads/2015/12/SylizedBarbariansMaleStrong.png");
			filters.Add("*//hasullivan*20??*.png");
			expected.Add(true);

			texts.Add("http://hasullivan.com/wp-content/uploads/2015/12/SylizedBarbariansFemale_Small.png");
			filters.Add("https*");
			expected.Add(false);

			texts.Add("http://hasullivan.com/wp-content/uploads/2015/12/SylizedBarbariansFemale_Small.png");
			filters.Add("http*wp-content*.???");
			expected.Add(true);

			texts.Add("https://mva.microsoft.com/training-topics/c-app-development#!jobf=Developer&lang=1033");
			filters.Add("http*microsoft.???*development*");
			expected.Add(true);

			texts.Add("https://mva.microsoft.com/en-US/training-courses/introduction-to-json-with-c-12742");
			filters.Add("*training-courses*introduction-to-*");
			expected.Add(true);

			texts.Add("https://mva.microsoft.com/en-US/training-courses/programming-in-c-jump-start-14254");
			filters.Add("https*programming*C#*");
			expected.Add(false);

			texts.Add("https://github.com/ValveSoftware/openvr");
			filters.Add("*s:*.???*vr");
			expected.Add(true);

			texts.Add("https://github.com/mono/MonoGame");
			filters.Add("*github.???????MonoGame");
			expected.Add(false);

			texts.Add("https://github.com/dotnet/roslyn");
			filters.Add("*dotnet*");
			expected.Add(true);

			texts.Add("https://github.com/dotnet/corefx");
			filters.Add("https*corefx");
			expected.Add(true);

			texts.Add("https://github.com/dotnet/cli");
			filters.Add("*clr*");
			expected.Add(false);

			texts.Add("uQ10mG10OeNbPW2X8Yet");
			filters.Add("uQ10mG10OeNdPW2X8Yet");
			expected.Add(false);

			texts.Add("CNInQBMi01ZmG3Ymb0pr");
			filters.Add("CN***??*G3*r");
			expected.Add(true);

			texts.Add("kVRglhOfmMqutfF020LW");
			filters.Add("**Ofm*M*tf?020L?");
			expected.Add(true);

			texts.Add("FIG3j1g8GSuhfyrfEgIK");
			filters.Add("*FIG??????????");
			expected.Add(false);

			texts.Add("<EMAIL>");
			filters.Add("*?@*.???");
			expected.Add(true);

			texts.Add("<h1>HTML Heading</h1>");
			filters.Add("*<h1>*</h1>*");
			expected.Add(true);

			texts.Add("Nabd");
			filters.Add("*.xxxN*");
			expected.Add(false);

			for (int i = 0; i < texts.Count; i++)
			{
				var txt = texts[i];
				var filter = filters[i];
				var facit = expected[i];
				var awc = new WildcardMatcher(filter);
				Assert.AreEqual(facit, awc.Matches(txt));

				// convert to regex and test that too
				var rx = "^" + filter.Replace("?", ".{1}");
				rx = rx.Replace("*", ".*");
				rx += "$";
				Assert.AreEqual(facit, System.Text.RegularExpressions.Regex.IsMatch(txt, rx));
			}
		}
	}
}


<|start_filename|>src/JobService/JobService.ForEachAction.cs<|end_filename|>
﻿using System;

namespace Lidgren.Core
{
	public static partial class JobService
	{
		/// <summary>
		/// Enqueue a list of actions to be performed concurrently; calls continuation(continuationArgument) when all completed
		/// </summary>
		public static void ForEachAction(string name, ReadOnlySpan<Action<object>> works, object argument, Action<object> continuation, object continuationArgument)
		{
			CoreException.Assert(s_workers != null, "JobService not initialized");

			int numJobs = works.Length;

			if (numJobs == 0)
				return;
			if (numJobs == 1)
			{
				lock(s_instances)
					EnqueueInternal(name, works[0], argument);
				return;
			}

			var completion = JobCompletion.Acquire();
			completion.ContinuationAtCount = numJobs;
			completion.Continuation = continuation;
			completion.ContinuationArgument = continuationArgument;
#if DEBUG
			completion.ContinuationName = name + "Contd";
#endif

			lock (s_instances)
			{
				foreach (var work in works)
					EnqueueInternal(name, work, argument, completion);
			}
		}

		/// <summary>
		/// Enqueue a list of actions to be performed concurrently; calls continuation(continuationArgument) when all completed
		/// </summary>
		public static void ForEachActionBlock(string name, ReadOnlySpan<Action<object>> works, object argument)
		{
			CoreException.Assert(s_workers != null, "JobService not initialized");

			if (works.Length == 0)
				return;
			if (works.Length == 1)
			{
				works[0](argument);
				return;
			}

			int numJobs = works.Length - 1;
			var completion = JobCompletion.Acquire();
			lock (s_instances)
			{
				foreach (var work in works.Slice(1))
					EnqueueInternal(name, work, argument, completion);
			}

			// run one time on this thread
			using (new Timing(name))
				works[0](argument);

			// then, try to steal relevant jobs if possible
			while (JobService.ExecuteAnyJob(null, completion))
				;

			completion.WaitAndRelease(numJobs);
		}
	}
}


<|start_filename|>src/SpanExtensions.cs<|end_filename|>
﻿using System;
using System.Buffers.Binary;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

namespace Lidgren.Core
{
	public static partial class SpanExtensions
	{
		/// <summary>
		/// Swap places of two ranges of a span, in-place
		/// Example:
		///   (pivot 4) 1234BBB becomes BBB1234
		///   (pivot 2) AABBBBB becomes BBBBBAA
		/// </summary>
		public static void SwapBlocks<T>(this Span<T> span, int pivot) where T : struct
		{
			span.Slice(0, pivot).Reverse();
			span.Slice(pivot, span.Length - pivot).Reverse();
			span.Reverse();
		}

		//
		// Byte encoding below; all methods "consumes" span space as they're written to/from
		//

		/// <summary>
		/// Returns new span of size count from the data, then reduces span to remaining data
		/// </summary>
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static byte ReadByte(ref this ReadOnlySpan<byte> span)
		{
			var retval = span[0];
			span = span.Slice(1);
			return retval;
		}

		/// <summary>
		/// Returns new span of size count from the data, then reduces span to remaining data
		/// </summary>
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static ReadOnlySpan<byte> ReadBytes(ref this ReadOnlySpan<byte> span, int count)
		{
			var retval = span.Slice(0, count);
			span = span.Slice(count);
			return retval;
		}

		/// <summary>
		/// Returns new span of size count from the data, then reduces span to remaining data
		/// </summary>
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static void ReadBytes(ref this ReadOnlySpan<byte> span, Span<byte> into)
		{
			span.Slice(0, into.Length).CopyTo(into);
			span = span.Slice(into.Length);
		}

		/// <summary>
		/// Writes a bool as a single byte and reduces span to remaining data
		/// </summary>
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static void WriteBool(ref this Span<byte> span, bool value)
		{
			span[0] = value ? (byte)1 : (byte)0;
			span = span.Slice(1);
		}

		/// <summary>
		/// Reads a bool from a single byte and reduces span to remaining data
		/// </summary>
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static bool ReadBool(ref this ReadOnlySpan<byte> span)
		{
			var retval = span[0] != 0 ? true : false;
			span = span.Slice(1);
			return retval;
		}

		/// <summary>
		/// Writes UInt16 as two bytes and reduces span to remaining data
		/// </summary>
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static void WriteUInt16(ref this Span<byte> span, ushort value)
		{
			BinaryPrimitives.WriteUInt16LittleEndian(span, value);
			span = span.Slice(2);
		}

		/// <summary>
		/// Reads UInt16 from two bytes and reduces span to remaining data
		/// </summary>
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static ushort ReadUInt16(ref this ReadOnlySpan<byte> span)
		{
			var retval = BinaryPrimitives.ReadUInt16LittleEndian(span);
			span = span.Slice(2);
			return retval;
		}

		/// <summary>
		/// Writes Int16 as two bytes and reduces span to remaining data
		/// </summary>
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static void WriteInt16(ref this Span<byte> span, short value)
		{
			BinaryPrimitives.WriteInt16LittleEndian(span, value);
			span = span.Slice(2);
		}

		/// <summary>
		/// Reads Int16 from two bytes and reduces span to remaining data
		/// </summary>
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static short ReadInt16(ref this ReadOnlySpan<byte> span)
		{
			var retval = BinaryPrimitives.ReadInt16LittleEndian(span);
			span = span.Slice(2);
			return retval;
		}

		/// <summary>
		/// Writes UInt32 as four bytes and reduces span to remaining data
		/// </summary>
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static void WriteUInt32(ref this Span<byte> span, uint value)
		{
			BinaryPrimitives.WriteUInt32LittleEndian(span, value);
			span = span.Slice(4);
		}

		/// <summary>
		/// Reads UInt32 from four bytes and reduces span to remaining data
		/// </summary>
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static uint ReadUInt32(ref this ReadOnlySpan<byte> span)
		{
			var retval = BinaryPrimitives.ReadUInt32LittleEndian(span);
			span = span.Slice(4);
			return retval;
		}

		/// <summary>
		/// Writes Int32 as four bytes and reduces span to remaining data
		/// </summary>
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static void WriteInt32(ref this Span<byte> span, int value)
		{
			BinaryPrimitives.WriteInt32LittleEndian(span, value);
			span = span.Slice(4);
		}

		/// <summary>
		/// Reads Int32 from four bytes and reduces span to remaining data
		/// </summary>
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static int ReadInt32(ref this ReadOnlySpan<byte> span)
		{
			var retval = BinaryPrimitives.ReadInt32LittleEndian(span);
			span = span.Slice(4);
			return retval;
		}

		/// <summary>
		/// Writes UInt64 as eight bytes and reduces span to remaining data
		/// </summary>
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static void WriteUInt64(ref this Span<byte> span, ulong value)
		{
			BinaryPrimitives.WriteUInt64LittleEndian(span, value);
			span = span.Slice(8);
		}

		/// <summary>
		/// Reads UInt64 from eight bytes and reduces span to remaining data
		/// </summary>
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static ulong ReadUInt64(ref this ReadOnlySpan<byte> span)
		{
			var retval = BinaryPrimitives.ReadUInt64LittleEndian(span);
			span = span.Slice(8);
			return retval;
		}

		/// <summary>
		/// Writes UInt64 as eight bytes and reduces span to remaining data
		/// </summary>
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static void WriteInt64(ref this Span<byte> span, long value)
		{
			BinaryPrimitives.WriteInt64LittleEndian(span, value);
			span = span.Slice(8);
		}

		/// <summary>
		/// Reads UInt64 from eight bytes and reduces span to remaining data
		/// </summary>
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static long ReadInt64(ref this ReadOnlySpan<byte> span)
		{
			var retval = BinaryPrimitives.ReadInt64LittleEndian(span);
			span = span.Slice(8);
			return retval;
		}

		/// <summary>
		/// Writes a Single as four bytes and reduces span to remaining data
		/// </summary>
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static void WriteSingle(ref this Span<byte> span, float value)
		{
#if NET5_0_OR_GREATER
			BinaryPrimitives.WriteSingleLittleEndian(span, value);
			span = span.Slice(4);
#else
			WriteUInt32(ref span, SingleUIntUnion.ReinterpretCast(value));
#endif
		}

		/// <summary>
		/// Reads Single from four bytes and reduces span to remaining data
		/// </summary>
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static float ReadSingle(ref this ReadOnlySpan<byte> span)
		{
#if NET5_0_OR_GREATER
			var retval = BinaryPrimitives.ReadSingleLittleEndian(span);
			span = span.Slice(4);
			return retval;
#else
			return SingleUIntUnion.ReinterpretCast(ReadUInt32(ref span));
#endif
		}

		/// <summary>
		/// Writes a Double as eight bytes and reduces span to remaining data
		/// </summary>
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static void WriteDouble(ref this Span<byte> span, double value)
		{
#if NET5_0_OR_GREATER
			BinaryPrimitives.WriteDoubleLittleEndian(span, value);
			span = span.Slice(8);
#else
			WriteUInt64(ref span, DoubleULongUnion.ReinterpretCast(value));
#endif
		}

		/// <summary>
		/// Reads Single from four bytes and reduces span to remaining data
		/// </summary>
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static double ReadDouble(ref this ReadOnlySpan<byte> span)
		{
#if NET5_0_OR_GREATER
			var retval = BinaryPrimitives.ReadDoubleLittleEndian(span);
			span = span.Slice(8);
			return retval;
#else
			return DoubleULongUnion.ReinterpretCast(ReadUInt64(ref span));
#endif
		}

		/// <summary>
		/// Writes a length-prefixed character array; returns true on success and false on not enough space in span (in which case it does not reduce span)
		/// </summary>
		public static bool WriteString(ref this Span<byte> span, ReadOnlySpan<char> str)
		{
			if (span.Length < 2)
				return false;

			if (str.Length < 1)
			{
				// zero sized string; unfortunately can't distinguish between null or String.Empty
				span[0] = 0;
				span[1] = 0;
				span = span.Slice(2);
				return true;
			}

			int remaining = span.Length;
			if (remaining < 2 + str.Length)
				return false; // assume we need at least one byte per character

			try
			{
				var work = span.Slice(2);
				if (remaining < str.Length)
					return false; // assume we need at least one byte per character

				int bytesWritten = System.Text.Encoding.UTF8.GetBytes(str, work);
				span[0] = (byte)bytesWritten;
				span[1] = (byte)(bytesWritten >> 8);
				span = span.Slice(2 + bytesWritten);
				return true;
			}
			catch
			{
				// ugh, exception, screw performance - verify exception is indeed not enough space
				int needBytes = 2 + System.Text.Encoding.UTF8.GetByteCount(str);
				if (needBytes > remaining)
					return false; // space issue; just return false

				// NOT a space issue - rethrow
				throw;
			}
		}

		/// <summary>
		/// Reads a length-prefixed character array written using WriteString() and reduces span to remaining data; returns number of characters read, or -1 if not enough space in 'into'
		/// </summary>
		public static int ReadString(ref this ReadOnlySpan<byte> data, Span<char> into)
		{
			int byteLen = (int)data[0] | ((int)data[1] << 8);
			var numChars = System.Text.Encoding.UTF8.GetCharCount(data.Slice(2, byteLen));
			if (into.Length < numChars)
				return -1;
			System.Text.Encoding.UTF8.GetChars(data.Slice(2), into);
			data = data.Slice(2 + byteLen);
			return numChars;
		}

		/// <summary>
		/// Reads a length-prefixed character array written using WriteString() and reduces span to remaining data
		/// </summary>
		public static string ReadString(ref this ReadOnlySpan<byte> data)
		{
			int byteLen = (int)data[0] | ((int)data[1] << 8);
			var retval = System.Text.Encoding.UTF8.GetString(data.Slice(2, byteLen));
			data = data.Slice(2 + byteLen);
			return retval;
		}

		/// <summary>
		/// Write an unmanaged struct (ie containing no reference types) and reduces span to remaining data
		/// </summary>
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static void Write<T>(ref this Span<byte> span, T value) where T : unmanaged
		{
			MemoryMarshal.Write<T>(span, ref value);
			span = span.Slice(Unsafe.SizeOf<T>());
		}

		/// <summary>
		/// Reads an unmanaged struct (ie containing no reference types) and reduces span to remaining data
		/// </summary>
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static T Read<T>(ref this ReadOnlySpan<byte> data) where T : unmanaged
		{
			var retval = MemoryMarshal.Read<T>(data);
			data = data.Slice(Unsafe.SizeOf<T>());
			return retval;
		}

		public static void WriteLengthPrefixedArray<TArrItem>(ref this Span<byte> span, ReadOnlySpan<TArrItem> items) where TArrItem : unmanaged
		{
			span.WriteVariableUInt32((uint)items.Length);
			if (items.Length == 0)
				return;
			var src = MemoryMarshal.AsBytes(items);
			src.CopyTo(span);
			span = span.Slice(src.Length);
		}

		/// <summary>
		/// Reads a length prefixed array written by WriteLengthPrefixedArray()
		/// </summary>
		public static TArrItem[] ReadLengthPrefixedArray<TArrItem>(ref this ReadOnlySpan<byte> data) where TArrItem : unmanaged
		{
			var itemsCount = (int)data.ReadVariableUInt32();
			var itemSize = Unsafe.SizeOf<TArrItem>();
			var numBytes = itemsCount * itemSize;

			var src = MemoryMarshal.Cast<byte, TArrItem>(data.Slice(0, numBytes));
			var retval = new TArrItem[itemsCount];
			src.CopyTo(retval);

			data = data.Slice(numBytes);
			return retval;
		}

		/// <summary>
		/// Reads Vector2 reduces span to remaining data
		/// </summary>
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static Vector2 ReadVector2(ref this ReadOnlySpan<byte> data)
		{
#if NET5_0_OR_GREATER
			Vector2 retval;
			retval.X = BinaryPrimitives.ReadSingleLittleEndian(data);
			retval.Y = BinaryPrimitives.ReadSingleLittleEndian(data.Slice(4));
			data = data.Slice(8);
			return retval;
#else
			Vector2 retval;
			retval.X = data.ReadSingle();
			retval.Y = data.ReadSingle();
			return retval;
#endif
		}

		/// <summary>
		/// Reads Vector3 reduces span to remaining data
		/// </summary>
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static Vector3 ReadVector3(ref this ReadOnlySpan<byte> data)
		{
#if NET5_0_OR_GREATER
			Vector3 retval;
			retval.X = BinaryPrimitives.ReadSingleLittleEndian(data);
			retval.Y = BinaryPrimitives.ReadSingleLittleEndian(data.Slice(4));
			retval.Z = BinaryPrimitives.ReadSingleLittleEndian(data.Slice(8));
			data = data.Slice(12);
			return retval;
#else
			Vector3 retval;
			retval.X = data.ReadSingle();
			retval.Y = data.ReadSingle();
			retval.Z = data.ReadSingle();
			return retval;
#endif
		}

		/// <summary>
		/// Reads Vector4 reduces span to remaining data
		/// </summary>
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static Vector4 ReadVector4(ref this ReadOnlySpan<byte> data)
		{
#if NET5_0_OR_GREATER
			Vector4 retval;
			retval.X = BinaryPrimitives.ReadSingleLittleEndian(data);
			retval.Y = BinaryPrimitives.ReadSingleLittleEndian(data.Slice(4));
			retval.Z = BinaryPrimitives.ReadSingleLittleEndian(data.Slice(8));
			retval.W = BinaryPrimitives.ReadSingleLittleEndian(data.Slice(12));
			data = data.Slice(16);
			return retval;
#else
			Vector4 retval;
			retval.X = data.ReadSingle();
			retval.Y = data.ReadSingle();
			retval.Z = data.ReadSingle();
			retval.W = data.ReadSingle();
			return retval;
#endif
		}
	}
}


<|start_filename|>src/Collections/PriorityQueue.cs<|end_filename|>
﻿using System;
using System.Diagnostics;

namespace Lidgren.Core
{
	/// <summary>
	/// Priority queue using a min binary heap
	/// </summary>
	public class PriorityQueue<TPriority, TItem> where TPriority : struct, IComparable<TPriority>
	{
		[DebuggerDisplay("{Prio} {Item}")]
		private struct Entry
		{
			public TPriority Prio;
			public TItem Item;
		}
		private Entry[] m_entries;

		private int m_count;
		public int Count => m_count;

		public PriorityQueue(int initialCapacity)
		{
			m_entries = new Entry[initialCapacity];
		}

		//private bool Validate()
		//{
		//	var ok = PeekPriority(out var nextPrio);
		//	if (ok)
		//	{
		//		for (int i = 0; i < m_count; i++)
		//		{
		//			ref var entry = ref m_entries[i];
		//			if (entry.Prio.CompareTo(nextPrio) < 0)
		//				return false;
		//			// CoreException.Throw("Queue corrupt");
		//
		//			// check heapness
		//			var left = (2 * i) + 1;
		//			if (left < m_count)
		//			{
		//				// verify left child is larger or equal
		//				if (m_entries[left].Prio.CompareTo(entry.Prio) < 0)
		//					return false;
		//			}
		//
		//			var right = (2 * i) + 2;
		//			if (right < m_count)
		//			{
		//				// verify left child is larger or equal
		//				if (m_entries[right].Prio.CompareTo(entry.Prio) < 0)
		//					return false;
		//			}
		//		}
		//	}
		//	return true;
		//}

		/// <summary>
		/// Enqueue an item with a certain priority
		/// </summary>
		public void Enqueue(TPriority prio, TItem item)
		{
			var entries = m_entries;

			var cnt = m_count;
			if (cnt >= entries.Length)
			{
				Array.Resize(ref m_entries, cnt * 2);
				entries = m_entries;
			}

			Entry entry;
			entry.Prio = prio;
			entry.Item = item;

			entries[cnt] = entry;
			int idx = cnt;
			cnt++;
			m_count = cnt;

			// bubble up
			while (idx > 0)
			{
				int parentIndex = (idx - 1) >> 1;
				ref readonly var parent = ref entries[parentIndex];
				if (parent.Prio.CompareTo(prio) <= 0)
					break;
				entries[idx] = parent;
				idx = parentIndex;
			}

			if (cnt > 0)
				entries[idx] = entry;
		}

		/// <summary>
		/// Peek what the next TryDequeue would result in
		/// </summary>
		public bool Peek(out TPriority nextDequeuedPrio, out TItem nextDequeuedItem)
		{
			if (m_count == 0)
			{
				nextDequeuedPrio = default;
				nextDequeuedItem = default;
				return false;
			}

			ref readonly var peek = ref m_entries[0];
			nextDequeuedPrio = peek.Prio;
			nextDequeuedItem = peek.Item;
			return true;
		}

		/// <summary>
		/// Peek what the next TryDequeue would result in
		/// </summary>
		public bool PeekPriority(out TPriority nextDequeuedPrio)
		{
			if (m_count == 0)
			{
				nextDequeuedPrio = default;
				return false;
			}

			ref readonly var peek = ref m_entries[0];
			nextDequeuedPrio = peek.Prio;
			return true;
		}

		/// <summary>
		/// Try dequeue the item with the lowest priority value; returns false if queue is empty or if the lowest priority is higher than threshold
		/// </summary>
		public bool TryDequeue(out TItem retval, TPriority threshold)
		{
			if (m_count == 0)
			{
				retval = default;
				return false;
			}

			ref readonly var peek = ref m_entries[0];
			if (peek.Prio.CompareTo(threshold) > 0)
			{
				retval = default;
				return false;
			}

			return TryDequeue(out retval);
		}

		/// <summary>
		/// Dequeues the item with the lowest priority value; throws exception if queue is empty
		/// </summary>
		public TItem Dequeue()
		{
			if (m_count == 0)
				CoreException.Throw("Queue is empty");
			var ok = TryDequeue(out var retval);
			return retval;
		}

		/// <summary>
		/// Try dequeue the item with the lowest priority value; returns false if queue is empty
		/// </summary>
		public bool TryDequeue(out TItem retval)
		{
			if (m_count == 0)
			{
				retval = default;
				return false;
			}

			retval = m_entries[0].Item;

			int cnt = m_count - 1;
			var replacement = m_entries[cnt];
			m_count = cnt;
			if (cnt == 0)
				return true;

			// pull up
			int index = 0;
			for (; ; )
			{
				var leftChildIndex = index * 2 + 1;
				if (leftChildIndex >= cnt)
					break;
				int rightChildIndex = index * 2 + 2;
				int swapIndex = rightChildIndex < cnt && m_entries[rightChildIndex].Prio.CompareTo(m_entries[leftChildIndex].Prio) < 0 ? rightChildIndex : leftChildIndex;

				ref readonly var cmp = ref m_entries[swapIndex];
				if (cmp.Prio.CompareTo(replacement.Prio) >= 0)
					break;

				m_entries[index] = cmp;
				index = swapIndex;
			}

			m_entries[index] = replacement;
			return true;
		}

		private int GetItemIndex(in TItem item)
		{
			int count = m_count;
			for (int i = 0; i < count; i++)
			{
				ref var entry = ref m_entries[i];
				if (item is null)
				{
					if (entry.Item is null)
						return i;
					continue;
				}
				if (item.Equals(entry.Item))
					return i;
			}
			return -1;
		}

		/// <summary>
		/// Remove first item found from queue
		/// </summary>
		public bool Remove(in TItem item)
		{
			var index = GetItemIndex(item);
			if (index == -1)
				return false;

			int cnt = m_count - 1;
			m_count = cnt;
			if (index == cnt)
				return true; // just drop it off the end

			if (cnt == 1)
			{
				// we removed top item
				CoreException.Assert(index == 0);
				m_entries[0] = m_entries[1];
				return true;
			}

			var replacement = m_entries[cnt];

			while (index > 0)
			{
				// check parent for upwards travelling
				var parentIndex = (index - 1) / 2;
				ref readonly var parentEntry = ref m_entries[parentIndex];
				if (replacement.Prio.CompareTo(parentEntry.Prio) < 0)
				{
					// need to swap up
					m_entries[index] = parentEntry;
					index = parentIndex;
					continue; // loop back; may need to travel higher yet
				}
				break;
			}

			// ok, check children
			for (; ; )
			{
				var leftChildIndex = index * 2 + 1;
				if (leftChildIndex >= cnt)
					break;
				int rightChildIndex = index * 2 + 2;

				ref readonly var leftChildEntry = ref m_entries[leftChildIndex];
				if (rightChildIndex < cnt)
				{
					ref readonly var rightChildEntry = ref m_entries[rightChildIndex];
					if (leftChildEntry.Prio.CompareTo(rightChildEntry.Prio) >= 0)
					{
						// right child is smaller
						if (rightChildEntry.Prio.CompareTo(replacement.Prio) < 0)
						{
							m_entries[index] = rightChildEntry;
							index = rightChildIndex;
							continue;
						}
						break;
					}
				}

				// left child is smaller
				if (leftChildEntry.Prio.CompareTo(replacement.Prio) < 0)
				{
					m_entries[index] = leftChildEntry;
					index = leftChildIndex;
					continue;
				}
				break;
			}

			m_entries[index] = replacement;
			return true;
		}

		/// <summary>
		/// Remove all items from the queue
		/// </summary>
		public void Clear()
		{
			m_count = 0;
			m_entries.AsSpan().Clear();
		}
	}
}


<|start_filename|>src/FourCC.cs<|end_filename|>
﻿#nullable enable
using System;
using System.Buffers.Binary;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

namespace Lidgren.Core
{
	/// <summary>
	/// Immutable 32 bit struct convertable to/from fourcc notation
	/// </summary>
	[DebuggerDisplay("{Readable}")]
	[StructLayout(LayoutKind.Sequential, Size = 4)]
	public readonly struct FourCC : IEquatable<FourCC>, IEquatable<uint>
	{
		public static readonly FourCC Empty = new FourCC(0);

		/// <summary>
		/// Big endian value; with regards to text
		/// </summary>
		public readonly uint Value;

		public string Readable => ToString();

		public FourCC(string str)
		{
			CoreException.Assert(str.Length >= 4);
			Value = (uint)((uint)str[3] << 24 | (uint)str[2] << 16 | (uint)str[1] << 8 | str[0]);
		}

		/// <summary>
		/// Pass bytes in reading order; ie { 'm', 'p', '4', 'a' }
		/// </summary>
		public FourCC(char a, char b, char c, char d)
		{
			Value = (uint)((uint)d << 24 | (uint)c << 16 | (uint)b << 8 | (uint)a);
		}

		public FourCC(ReadOnlySpan<byte> data)
		{
			CoreException.Assert(data.Length >= 4);
			Value = (uint)((uint)data[3] << 24 | (uint)data[2] << 16 | (uint)data[1] << 8 | (uint)data[0]);
		}

		/// <summary>
		/// Note; value should be big endian
		/// </summary>
		public FourCC(uint value)
		{
			Value = value;
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void ToString(Span<char> into)
		{
			into[0] = (char)(Value & 255u);
			into[1] = (char)(Value >> 8 & 255u);
			into[2] = (char)(Value >> 16 & 255u);
			into[3] = (char)(Value >> 24 & 255u);
		}

		public override string ToString()
		{
			Span<char> data = stackalloc char[4];
			ToString(data);
			return data.ToString();
		}

		public bool Equals(FourCC other)
		{
			return this.Value == other.Value;
		}

		public bool Equals(ReadOnlySpan<byte> bytes)
		{
			var value = BinaryPrimitives.ReadUInt32BigEndian(bytes);
			return value == Value;
		}

		public bool Equals(ReadOnlySpan<char> text)
		{
			CoreException.Assert(text.Length == 4);
			if (text.Length < 4)
				return false;

			Span<char> arr = stackalloc char[4];
			ToString(arr);

			return arr.SequenceEqual(text);
		}

		public override bool Equals(object? obj)
		{
			return obj != null && obj is FourCC && this.Equals((FourCC)obj);
		}

		public override int GetHashCode()
		{
			return (int)Value;
		}

		public bool Equals(uint other)
		{
			return other == Value;
		}

		public static bool operator ==(FourCC left, FourCC right)
		{
			return left.Value == right.Value;
		}

		public static bool operator !=(FourCC left, FourCC right)
		{
			return left.Value != right.Value;
		}

		public static bool operator ==(FourCC left, ReadOnlySpan<char> right)
		{
			return left.Equals(right);
		}

		public static bool operator !=(FourCC left, ReadOnlySpan<char> right)
		{
			return !left.Equals(right);
		}

		public static implicit operator uint(FourCC cc)
		{
			return cc.Value;
		}

		public static implicit operator FourCC(uint value)
		{
			return new FourCC(value);
		}
	}
}


<|start_filename|>src/CoreException.cs<|end_filename|>
﻿#nullable enable
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Numerics;
using System.Runtime.CompilerServices;

namespace Lidgren.Core
{
	public sealed class CoreException : Exception
	{
#if DEBUG
		private static readonly HashSet<object> s_runOnceHasRan = new HashSet<object>();
#endif

		public CoreException()
			: base()
		{
		}

		public CoreException(string? message)
			: base(message)
		{
		}

		public CoreException(string? message, Exception? inner)
			: base(message, inner)
		{
		}

		/// <summary>
		/// Throws an exception, in DEBUG only, if first parameter is false
		/// </summary>
		[Conditional("DEBUG")]
		public static void Assert(bool isOk, string message = "")
		{
			if (isOk == false)
			{
#if DEBUG
				if (Debugger.IsAttached)
					Debugger.Break();
				else
					CoreException.Throw(message);
#else
				CoreException.Throw(message);
#endif
			}
		}


		/// <summary>
		/// Throws an exception, in DEBUG AND RELEASE, if first parameter is false
		/// </summary>
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static void FatalAssert(bool isOk, string message = "")
		{
			if (isOk == false)
				CoreException.Throw(message);
		}

		[MethodImpl(MethodImplOptions.NoInlining)]
		public static void Throw(string? message = null, Exception? inner = null)
		{
#if DEBUG
			if (Debugger.IsAttached)
				Debugger.Break();
#endif
			throw new CoreException(message, inner);
		}

		[MethodImpl(MethodImplOptions.NoInlining)]
		public static void ThrowNotImplemented(string? message = null)
		{
			throw new CoreException(message is null ? "Not implemented" : message);
		}

		/// <summary>
		/// Throw exception, in DEBUG only, if this identifier runs more than once
		/// </summary>
		[Conditional("DEBUG")]
		public static void AssertRunOnce(object identifier)
		{
#if DEBUG
			lock (identifier)
			{
				if (s_runOnceHasRan.Contains(identifier))
				{
					Throw($"Identifier {identifier} ran more than once!");
					return;
				}
				s_runOnceHasRan.Add(identifier);
			}
#endif
		}

		[Conditional("DEBUG")]
		public static void CheckNaN(Vector3 vector)
		{
			CoreException.Assert(
				float.IsNaN(vector.X) == false &&
				float.IsNaN(vector.Y) == false &&
				float.IsNaN(vector.Z) == false &&

				float.IsInfinity(vector.X) == false &&
				float.IsInfinity(vector.Y) == false &&
				float.IsInfinity(vector.Z) == false
			);
		}

		[Conditional("DEBUG")]
		public static void CheckNaN(float vector)
		{
			CoreException.Assert(float.IsNaN(vector) == false);
		}

		[Conditional("DEBUG")]
		public static void CheckNaN(Quaternion vector)
		{
			CoreException.Assert(
				float.IsNaN(vector.X) == false &&
				float.IsNaN(vector.Z) == false &&
				float.IsNaN(vector.Y) == false &&
				float.IsNaN(vector.W) == false &&
				float.IsInfinity(vector.X) == false &&
				float.IsInfinity(vector.Y) == false &&
				float.IsInfinity(vector.Z) == false
			);
		}

		[Conditional("DEBUG")]
		public static void CheckNaN(Vector2 vector)
		{
			CoreException.Assert(
				float.IsNaN(vector.X) == false &&
				float.IsNaN(vector.Y) == false &&

				float.IsInfinity(vector.X) == false &&
				float.IsInfinity(vector.Y) == false
			);
		}
	}
}


<|start_filename|>tests/HasherTests.cs<|end_filename|>
﻿using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;

using Lidgren.Core;
using System.Collections.Generic;

namespace UnitTests
{
	[TestClass]
	public class HasherTests
	{
		[TestMethod]
		public void TestHasher()
		{
			var data = new byte[240];
			for (int i = 0; i < data.Length; i++)
				data[i] = (byte)i;

			var seen32 = new HashSet<uint>();
			var seen64 = new HashSet<ulong>();

			var hasher = Hasher.Create();
			for (int len = 1; len < data.Length; len++)
			{
				hasher.Reset();
				hasher.Add(data.AsSpan(0, len));
				var h32 = hasher.Finalize32();
				Assert.IsFalse(seen32.Contains(h32));

				hasher.Reset();
				hasher.Add(data.AsSpan(0, len));
				var h64 = hasher.Finalize64();
				Assert.IsFalse(seen64.Contains(h64));
			}

			{
				hasher.Reset();
				hasher.Add("abc");
				var one = hasher.Finalize64();
				hasher.Reset();
				hasher.AddLower("AbC");
				var two = hasher.Finalize64();
				Assert.AreEqual(one, two);
			}

			{
				hasher.Reset();
				hasher.Add((byte)1);
				hasher.Add((byte)2);
				var one = hasher.Finalize64();

				hasher.Reset();
				hasher.Add((byte)2);
				hasher.Add((byte)1);
				var two = hasher.Finalize64();

				Assert.AreNotEqual(one, two);
			}

			{
				var hashes = new HashSet<ulong>();
				foreach (var word in s_words)
				{
					var h = HashUtil.FNV1a(word);
					var added = hashes.Add(h);
					Assert.IsTrue(added);
				}
			}

			{
				var hashes = new HashSet<ulong>();
				foreach (var word in s_words)
				{
					var h = HashUtil.Hash32(word);
					var added = hashes.Add(h);
					Assert.IsTrue(added);
				}
			}

			{
				var hashes = new HashSet<ulong>();
				foreach (var word in s_words)
				{
					var h = HashUtil.Hash64(word);
					var added = hashes.Add(h);
					Assert.IsTrue(added);
				}
			}

			{
				var hashes = new HashSet<ulong>();
				foreach (var word in s_words)
				{
					var bytes = System.Text.Encoding.UTF8.GetBytes(word);
					var h = HashUtil.FNV1a64(bytes);
					var added = hashes.Add(h);
					Assert.IsTrue(added);
				}
			}
		}

		private static string[] s_words = new string[]
		{
"a",
"ability",
"able",
"about",
"above",
"accept",
"according",
"account",
"across",
"act",
"action",
"activity",
"actually",
"add",
"address",
"administration",
"admit",
"adult",
"affect",
"after",
"again",
"against",
"age",
"agency",
"agent",
"ago",
"agree",
"agreement",
"ahead",
"air",
"all",
"allow",
"almost",
"alone",
"along",
"already",
"also",
"although",
"always",
"American",
"among",
"amount",
"analysis",
"and",
"animal",
"another",
"answer",
"any",
"anyone",
"anything",
"appear",
"apply",
"approach",
"area",
"argue",
"arm",
"around",
"arrive",
"art",
"article",
"artist",
"as",
"ask",
"assume",
"at",
"attack",
"attention",
"attorney",
"audience",
"author",
"authority",
"available",
"avoid",
"away",
"baby",
"back",
"bad",
"bag",
"ball",
"bank",
"bar",
"base",
"be",
"beat",
"beautiful",
"because",
"become",
"bed",
"before",
"begin",
"behavior",
"behind",
"believe",
"benefit",
"best",
"better",
"between",
"beyond",
"big",
"bill",
"billion",
"bit",
"black",
"blood",
"blue",
"board",
"body",
"book",
"born",
"both",
"box",
"boy",
"break",
"bring",
"brother",
"budget",
"build",
"building",
"business",
"but",
"buy",
"by",
"call",
"camera",
"campaign",
"can",
"cancer",
"candidate",
"capital",
"car",
"card",
"care",
"career",
"carry",
"case",
"catch",
"cause",
"cell",
"center",
"central",
"century",
"certain",
"certainly",
"chair",
"challenge",
"chance",
"change",
"character",
"charge",
"check",
"child",
"choice",
"choose",
"church",
"citizen",
"city",
"civil",
"claim",
"class",
"clear",
"clearly",
"close",
"coach",
"cold",
"collection",
"college",
"color",
"come",
"commercial",
"common",
"community",
"company",
"compare",
"computer",
"concern",
"condition",
"conference",
"Congress",
"consider",
"consumer",
"contain",
"continue",
"control",
"cost",
"could",
"country",
"couple",
"course",
"court",
"cover",
"create",
"crime",
"cultural",
"culture",
"cup",
"current",
"customer",
"cut",
"dark",
"data",
"daughter",
"day",
"dead",
"deal",
"death",
"debate",
"decade",
"decide",
"decision",
"deep",
"defense",
"degree",
"Democrat",
"democratic",
"describe",
"design",
"despite",
"detail",
"determine",
"develop",
"development",
"die",
"difference",
"different",
"difficult",
"dinner",
"direction",
"director",
"discover",
"discuss",
"discussion",
"disease",
"do",
"doctor",
"dog",
"door",
"down",
"draw",
"dream",
"drive",
"drop",
"drug",
"during",
"each",
"early",
"east",
"easy",
"eat",
"economic",
"economy",
"edge",
"education",
"effect",
"effort",
"eight",
"either",
"election",
"else",
"employee",
"end",
"energy",
"enjoy",
"enough",
"enter",
"entire",
"environment",
"environmental",
"especially",
"establish",
"even",
"evening",
"event",
"ever",
"every",
"everybody",
"everyone",
"everything",
"evidence",
"exactly",
"example",
"executive",
"exist",
"expect",
"experience",
"expert",
"explain",
"eye",
"face",
"fact",
"factor",
"fail",
"fall",
"family",
"far",
"fast",
"father",
"fear",
"federal",
"feel",
"feeling",
"few",
"field",
"fight",
"figure",
"fill",
"film",
"final",
"finally",
"financial",
"find",
"fine",
"finger",
"finish",
"fire",
"firm",
"first",
"fish",
"five",
"floor",
"fly",
"focus",
"follow",
"food",
"foot",
"for",
"force",
"foreign",
"forget",
"form",
"former",
"forward",
"four",
"free",
"friend",
"from",
"front",
"full",
"fund",
"future",
"game",
"garden",
"gas",
"general",
"generation",
"get",
"girl",
"give",
"glass",
"go",
"goal",
"good",
"government",
"great",
"green",
"ground",
"group",
"grow",
"growth",
"guess",
"gun",
"guy",
"hair",
"half",
"hand",
"hang",
"happen",
"happy",
"hard",
"have",
"he",
"head",
"health",
"hear",
"heart",
"heat",
"heavy",
"help",
"her",
"here",
"herself",
"high",
"him",
"himself",
"his",
"history",
"hit",
"hold",
"home",
"hope",
"hospital",
"hot",
"hotel",
"hour",
"house",
"how",
"however",
"huge",
"human",
"hundred",
"husband",
"I",
"idea",
"identify",
"if",
"image",
"imagine",
"impact",
"important",
"improve",
"in",
"include",
"including",
"increase",
"indeed",
"indicate",
"individual",
"industry",
"information",
"inside",
"instead",
"institution",
"interest",
"interesting",
"international",
"interview",
"into",
"investment",
"involve",
"issue",
"it",
"item",
"its",
"itself",
"job",
"join",
"just",
"keep",
"key",
"kid",
"kill",
"kind",
"kitchen",
"know",
"knowledge",
"land",
"language",
"large",
"last",
"late",
"later",
"laugh",
"law",
"lawyer",
"lay",
"lead",
"leader",
"learn",
"least",
"leave",
"left",
"leg",
"legal",
"less",
"let",
"letter",
"level",
"lie",
"life",
"light",
"like",
"likely",
"line",
"list",
"listen",
"little",
"live",
"local",
"long",
"look",
"lose",
"loss",
"lot",
"love",
"low",
"machine",
"magazine",
"main",
"maintain",
"major",
"majority",
"make",
"man",
"manage",
"management",
"manager",
"many",
"market",
"marriage",
"material",
"matter",
"may",
"maybe",
"me",
"mean",
"measure",
"media",
"medical",
"meet",
"meeting",
"member",
"memory",
"mention",
"message",
"method",
"middle",
"might",
"military",
"million",
"mind",
"minute",
"miss",
"mission",
"model",
"modern",
"moment",
"money",
"month",
"more",
"morning",
"most",
"mother",
"mouth",
"move",
"movement",
"movie",
"Mr",
"Mrs",
"much",
"music",
"must",
"my",
"myself",
"name",
"nation",
"national",
"natural",
"nature",
"near",
"nearly",
"necessary",
"need",
"network",
"never",
"new",
"news",
"newspaper",
"next",
"nice",
"night",
"no",
"none",
"nor",
"north",
"not",
"note",
"nothing",
"notice",
"now",
"n't",
"number",
"occur",
"of",
"off",
"offer",
"office",
"officer",
"official",
"often",
"oh",
"oil",
"ok",
"old",
"on",
"once",
"one",
"only",
"onto",
"open",
"operation",
"opportunity",
"option",
"or",
"order",
"organization",
"other",
"others",
"our",
"out",
"outside",
"over",
"own",
"owner",
"page",
"pain",
"painting",
"paper",
"parent",
"part",
"participant",
"particular",
"particularly",
"partner",
"party",
"pass",
"past",
"patient",
"pattern",
"pay",
"peace",
"people",
"per",
"perform",
"performance",
"perhaps",
"period",
"person",
"personal",
"phone",
"physical",
"pick",
"picture",
"piece",
"place",
"plan",
"plant",
"play",
"player",
"PM",
"point",
"police",
"policy",
"political",
"politics",
"poor",
"popular",
"population",
"position",
"positive",
"possible",
"power",
"practice",
"prepare",
"present",
"president",
"pressure",
"pretty",
"prevent",
"price",
"private",
"probably",
"problem",
"process",
"produce",
"product",
"production",
"professional",
"professor",
"program",
"project",
"property",
"protect",
"prove",
"provide",
"public",
"pull",
"purpose",
"push",
"put",
"quality",
"question",
"quickly",
"quite",
"race",
"radio",
"raise",
"range",
"rate",
"rather",
"reach",
"read",
"ready",
"real",
"reality",
"realize",
"really",
"reason",
"receive",
"recent",
"recently",
"recognize",
"record",
"red",
"reduce",
"reflect",
"region",
"relate",
"relationship",
"religious",
"remain",
"remember",
"remove",
"report",
"represent",
"Republican",
"require",
"research",
"resource",
"respond",
"response",
"responsibility",
"rest",
"result",
"return",
"reveal",
"rich",
"right",
"rise",
"risk",
"road",
"rock",
"role",
"room",
"rule",
"run",
"safe",
"same",
"save",
"say",
"scene",
"school",
"science",
"scientist",
"score",
"sea",
"season",
"seat",
"second",
"section",
"security",
"see",
"seek",
"seem",
"sell",
"send",
"senior",
"sense",
"series",
"serious",
"serve",
"service",
"set",
"seven",
"several",
"sex",
"sexual",
"shake",
"share",
"she",
"shoot",
"short",
"shot",
"should",
"shoulder",
"show",
"side",
"sign",
"significant",
"similar",
"simple",
"simply",
"since",
"sing",
"single",
"sister",
"sit",
"site",
"situation",
"six",
"size",
"skill",
"skin",
"small",
"smile",
"so",
"social",
"society",
"soldier",
"some",
"somebody",
"someone",
"something",
"sometimes",
"son",
"song",
"soon",
"sort",
"sound",
"source",
"south",
"southern",
"space",
"speak",
"special",
"specific",
"speech",
"spend",
"sport",
"spring",
"staff",
"stage",
"stand",
"standard",
"star",
"start",
"state",
"statement",
"station",
"stay",
"step",
"still",
"stock",
"stop",
"store",
"story",
"strategy",
"street",
"strong",
"structure",
"student",
"study",
"stuff",
"style",
"subject",
"success",
"successful",
"such",
"suddenly",
"suffer",
"suggest",
"summer",
"support",
"sure",
"surface",
"system",
"table",
"take",
"talk",
"task",
"tax",
"teach",
"teacher",
"team",
"technology",
"television",
"tell",
"ten",
"tend",
"term",
"test",
"than",
"thank",
"that",
"the",
"their",
"them",
"themselves",
"then",
"theory",
"there",
"these",
"they",
"thing",
"think",
"third",
"this",
"those",
"though",
"thought",
"thousand",
"threat",
"three",
"through",
"throughout",
"throw",
"thus",
"time",
"to",
"today",
"together",
"tonight",
"too",
"top",
"total",
"tough",
"toward",
"town",
"trade",
"traditional",
"training",
"travel",
"treat",
"treatment",
"tree",
"trial",
"trip",
"trouble",
"true",
"truth",
"try",
"turn",
"TV",
"two",
"type",
"under",
"understand",
"unit",
"until",
"up",
"upon",
"us",
"use",
"usually",
"value",
"various",
"very",
"victim",
"view",
"violence",
"visit",
"voice",
"vote",
"wait",
"walk",
"wall",
"want",
"war",
"watch",
"water",
"way",
"we",
"weapon",
"wear",
"week",
"weight",
"well",
"west",
"western",
"what",
"whatever",
"when",
"where",
"whether",
"which",
"while",
"white",
"who",
"whole",
"whom",
"whose",
"why",
"wide",
"wife",
"will",
"win",
"wind",
"window",
"wish",
"with",
"within",
"without",
"woman",
"wonder",
"word",
"work",
"worker",
"world",
"worry",
"would",
"write",
"writer",
"wrong",
"yard",
"yeah",
"year",
"yes",
"yet",
"you",
"young",
"your",
"yourself"
		};
	}
}


<|start_filename|>src/Collections/FastList/FastList.Json.cs<|end_filename|>
﻿using System;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace Lidgren.Core
{
	/// <summary>
	/// Usage: Add instance of this class to myJsonSerializerOptions.Converters
	/// </summary>
	public class JsonConverterFactoryForFastListOfT : JsonConverterFactory
	{
		public override bool CanConvert(Type typeToConvert)
			=> typeToConvert.IsGenericType
			&& typeToConvert.GetGenericTypeDefinition() == typeof(FastList<>);

		public override JsonConverter CreateConverter(
			Type typeToConvert, JsonSerializerOptions options)
		{
			CoreException.Assert(typeToConvert.IsGenericType && typeToConvert.GetGenericTypeDefinition() == typeof(FastList<>));

			Type elementType = typeToConvert.GetGenericArguments()[0];

			JsonConverter converter = (JsonConverter)Activator.CreateInstance(
				typeof(JsonConverterForFastListOfT<>)
					.MakeGenericType(new Type[] { elementType }),
				BindingFlags.Instance | BindingFlags.Public,
				binder: null,
				args: null,
				culture: null)!;

			return converter;
		}
	}

	public class JsonConverterForFastListOfT<T> : JsonConverter<FastList<T>>
	{
		public override FastList<T> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
		{
			if (reader.TokenType != JsonTokenType.StartArray)
			{
				throw new JsonException();
			}
			reader.Read();

			var elements = new FastList<T>();

			while (reader.TokenType != JsonTokenType.EndArray)
			{
				elements.Add(JsonSerializer.Deserialize<T>(ref reader, options));
				reader.Read();
			}

			return elements;
		}

		public override void Write(Utf8JsonWriter writer, FastList<T> value, JsonSerializerOptions options)
		{
			writer.WriteStartArray();

			var span = value.ReadOnlySpan;
			foreach(ref readonly var item in span)
				JsonSerializer.Serialize(writer, item, options);

			writer.WriteEndArray();
		}
	}
}


<|start_filename|>src/JobService/JobCompletion.cs<|end_filename|>
﻿using System;
using System.Threading;

namespace Lidgren.Core
{
	internal sealed class JobCompletion
	{
		private static FastList<JobCompletion> s_free = new FastList<JobCompletion>(4);
#if DEBUG
		private static int s_totalNumCreated = 0;
#endif
		private int m_numCompleted;

		public volatile int ContinuationAtCount;
		public Action<object> Continuation;
		public string ContinuationName;
		public object ContinuationArgument;

		public void WaitAndRelease(int completionCount)
		{
			CoreException.Assert(Continuation == null, "Can't use WaitAndRelease while also using a continuation");
			for (; ; )
			{
				var comp = Volatile.Read(ref m_numCompleted);
				if (comp >= completionCount)
					break;
				Thread.Sleep(0);
			}
			JobCompletion.Release(this);
		}

		public static JobCompletion Acquire()
		{
			lock (s_free)
			{
				if (s_free.TryPop(out var retval))
				{
					retval.ResetForReuse();
					return retval;
				}
#if DEBUG
				s_totalNumCreated++;
				CoreException.Assert(s_totalNumCreated < 64, "Unlikely high number of in-flight completions; leaking Completion objects?");
#endif
				return new JobCompletion();
			}
		}

		private void ResetForReuse()
		{
			m_numCompleted = 0;
			ContinuationAtCount = 0;
			Continuation = null;
			ContinuationName = null;
			ContinuationArgument = null;
		}

		public static void Release(JobCompletion completion)
		{
			lock (s_free)
			{
				s_free.Add(completion);
			}
		}

		internal void IncrementCompleted()
		{
			int cac = ContinuationAtCount;
			var numCompleted = Interlocked.Increment(ref m_numCompleted);
			if (numCompleted == cac && Continuation != null)
			{
				using (new Timing(ContinuationName == null ? "continuation" : ContinuationName))
				{
					var post = Continuation;
					var postArg = ContinuationArgument;
					JobCompletion.Release(this);
					post(postArg);
				}
			}
		}
	}
}


<|start_filename|>src/JobService/JobService.Delayed.cs<|end_filename|>
﻿using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;

namespace Lidgren.Core
{
	public static partial class JobService
	{
		private static readonly PriorityQueue<long, Job> s_delayed = new PriorityQueue<long, Job>(16);

		public static void Enqueue(string name, Action<object> work, object argument, double delaySeconds)
		{
			long earliest = Stopwatch.GetTimestamp() + TimeService.SecondsToTicks(delaySeconds);
			var djob = new Job() { Name = name, Work = work, Argument = argument };
			lock (s_delayed)
				s_delayed.Enqueue(earliest, djob);
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		private static bool PopDelayed(out Job job)
		{
			var now = Stopwatch.GetTimestamp();
			lock (s_delayed)
				return s_delayed.TryDequeue(out job, now);
		}
	}
}


<|start_filename|>src/DynamicScheduler.cs<|end_filename|>
﻿using System;
using System.Diagnostics;
using System.Threading;

namespace Lidgren.Core
{
	public enum ScheduleOrder
	{
		RunBefore = 0,
		AnyOrderConcurrent = 1,
		AnyOrderNotConcurrent = 2,
		RunAfter = 3,
	}

	public enum ScheduleHint
	{
		VeryEarly,
		Early,
		Default,
		Late,
		VeryLate,
	}

	public interface IScheduleItem<TItem, TArg>
	{
		string Name { get; }
		ScheduleHint Hint { get; }
		ScheduleOrder ScheduleAgainst(TItem other);
		void Execute(TArg argument);
	}

	public class DynamicScheduler<TItem, TArg> where TItem : IScheduleItem<TItem, TArg> where TArg : class
	{
		[DebuggerDisplay("{Item.Name} {NodsReceived}/{NodsRequired} nods")]
		private class ItemInfo : IDynamicOrdered
		{
			public int Index;

			public TItem Item;

			public int NodsRequired; // how many nods needed before we can run this job?
			public int NodsReceived; // how many nods has happened so far?

			public int EmitNodsCount; // number of nods this item emits
			public int EmitNodsIndex; // where in m_nodEmits to start looking

			public DynamicOrder OrderAgainst(object other)
			{
				var ot = (ItemInfo)other;
				var aOrder = Item.ScheduleAgainst(ot.Item);
				var bOrder = ot.Item.ScheduleAgainst(Item);
				aOrder = ScheduleOrderUtil.ResolveOrder(aOrder, bOrder, out var conflict);
				if (conflict)
					CoreException.Throw("Bad dynamic order");

				if (aOrder == ScheduleOrder.RunBefore)
					return DynamicOrder.RequireABeforeB;
				if (aOrder == ScheduleOrder.RunAfter)
					return DynamicOrder.RequireBBeforeA;

				// ok, soft order by hint
				var diff = (int)Item.Hint - (int)ot.Item.Hint;
				if (diff > 0)
					return DynamicOrder.PreferBBeforeA;
				if (diff < 0)
					return DynamicOrder.PreferABeforeB;
				return DynamicOrder.AnyOrder;
			}
		}

		private ItemInfo[] m_items;
		private TArg m_runArgument;
		private int[] m_nodEmits;
		private Action<object> m_executeItemAction;
		private SymmetricMatrixBool m_concurrencyPreventionMatrix;
		private int m_unprocessedItems;
		private Action<object> m_completedAction;
		private object m_completedArgument;

		private FastList<ItemInfo> m_running;

		public DynamicScheduler(ReadOnlySpan<TItem> scheduleItems)
		{
			m_executeItemAction = ExecuteScheduledItem;

			var items = new ItemInfo[scheduleItems.Length];
			for (int i = 0; i < items.Length; i++)
				items[i] = new ItemInfo() { Item = scheduleItems[i] };

			// sort dynamically
			var ok = DynamicOrdering.Perform<ItemInfo>(items, out _);
			if (!ok)
				CoreException.Throw("Failed to sort dynamically");

			m_concurrencyPreventionMatrix = new SymmetricMatrixBool(scheduleItems.Length);
			m_running = new FastList<ItemInfo>(16);

			// set up items (determine nods, concurrency etc)
			var nods = new FastList<int>(32);
			for (int aIdx = 0; aIdx < items.Length; aIdx++)
			{
				var a = items[aIdx];
				a.Index = aIdx; // set up index
				ref var aInfo = ref items[aIdx];
				aInfo.EmitNodsIndex = nods.Count;
				for (int bIdx = 0; bIdx < items.Length; bIdx++)
				{
					if (aIdx == bIdx)
						continue;

					// determine is aIdx nods to bIdx
					var b = items[bIdx];
					var aOrder = a.Item.ScheduleAgainst(b.Item);
					var bOrder = b.Item.ScheduleAgainst(a.Item);
					aOrder = ScheduleOrderUtil.ResolveOrder(aOrder, bOrder, out var conflict);
					if (aOrder == ScheduleOrder.RunBefore)
					{
						ref var bInfo = ref items[bIdx];
						bInfo.NodsRequired++;
						nods.Add(bIdx);
					}

					if (aOrder != ScheduleOrder.AnyOrderConcurrent)
						m_concurrencyPreventionMatrix[aIdx, bIdx] = true;
				}
				aInfo.EmitNodsCount = nods.Count - aInfo.EmitNodsIndex;
			}
			m_nodEmits = nods.ToArray();

			//for (int aIdx = 0; aIdx < items.Length; aIdx++)
			//{
			//	var a = items[aIdx];
			//	System.Diagnostics.Trace.WriteLine("Item: " + a.Item.Name);
			//	ref var aInfo = ref items[aIdx];
			//	if (aInfo.NodsRequired > 0)
			//		System.Diagnostics.Trace.WriteLine(a.Item.Name + " requires " + aInfo.NodsRequired + " nods");
			//	for (int i = 0; i < aInfo.EmitNodsCount; i++)
			//		System.Diagnostics.Trace.WriteLine(a.Item.Name + " nods to " + items[nods[aInfo.EmitNodsIndex + i]].Item.Name);
			//}

			m_items = items;
		}

		public void Execute(TArg argument, Action<object> completed, object completionArgument)
		{
			using var _ = new Timing("SetupScheduler");

			CoreException.Assert(m_unprocessedItems == 0);
			CoreException.Assert(m_completedAction == null);
			//System.Diagnostics.Trace.WriteLine("Starting scheduler");

			m_runArgument = argument;
			var items = m_items.AsSpan();

			m_completedAction = completed;
			m_completedArgument = completionArgument;
			m_unprocessedItems = items.Length;

			// reset all items
			CoreException.Assert(m_running.Count == 0);
			for (int i = 0; i < items.Length; i++)
			{
				ref var item = ref items[i];
				item.NodsReceived = 0;
			}

			// kick off all jobs with no dependencies
			lock (m_serial)
			{
				for (int i = 0; i < items.Length; i++)
				{
					ref var item = ref items[i];
					if (item.NodsRequired == 0)
					{
						if (CanRunNow(item))
						{
							lock (m_running)
								m_running.Add(item);
							JobService.Enqueue(item.Item.Name, ExecuteScheduledItem, item);
						}
						else
						{
							m_concurrentBlockedItems.Add(item);
						}
					}
				}
			}
		}

		private object m_serial = new object();
		private FastList<ItemInfo> m_concurrentBlockedItems = new FastList<ItemInfo>(16);

		private void ExecuteScheduledItem(object ob)
		{
			var item = (ItemInfo)ob;

			//System.Diagnostics.Trace.WriteLine("Executing " + item.Item.Name);

			// go!
			item.Item.Execute(m_runArgument);

			using (new Timing("scheduling"))
			{
				// set done state
				lock (m_running)
				{
					bool removed = m_running.Remove(item);
					CoreException.Assert(removed);
				}

				var unprocessedLeft = Interlocked.Decrement(ref m_unprocessedItems);
				CoreException.Assert(unprocessedLeft >= 0);

				if (unprocessedLeft == 0)
				{
					var ca = m_completedAction;
					if (ca != null)
					{
						var arg = m_completedArgument;
						m_completedAction = null;
						m_completedArgument = null;

						// schedule on jobservice instead of running directly; mostly for prettier trace
						JobService.Enqueue(ca, arg);
					}
					//System.Diagnostics.Trace.WriteLine("Schedule done!");
					return;
				}

				var items = m_items;

				Span<int> nodReleasedItems = stackalloc int[32];
				int numNodReleasedItems = 0;

				// emit nods
				for (int n = 0; n < item.EmitNodsCount; n++)
				{
					var noddedIdx = m_nodEmits[item.EmitNodsIndex + n];
					ref var noddedItem = ref items[noddedIdx];

					var nodSum = Interlocked.Increment(ref noddedItem.NodsReceived);

					//System.Diagnostics.Trace.WriteLine(item.Item.Name + " nods to " + noddedItem.Item.Name);

					CoreException.Assert(noddedItem.NodsRequired > 0);
					CoreException.Assert(nodSum <= noddedItem.NodsRequired);
					if (nodSum == noddedItem.NodsRequired)
					{
						CoreException.Assert(m_concurrentBlockedItems.Contains(noddedItem) == false);

						// store for checking later; under lock
						nodReleasedItems[numNodReleasedItems++] = noddedItem.Index;
					}
				}

				// check readyJobs if out finishing has enabled a ready job to run
				using (new LockTiming("chk", m_serial))
				{
					var concBlocked = m_concurrentBlockedItems;

					// first check all previously blocked items
					for (int i = 0; i < concBlocked.Count; i++)
					{
						var testItem = concBlocked[i];
						bool canRunNow = CanRunNow(testItem);
						if (canRunNow)
						{
							// no concurrency issue - run it!
							lock (m_running)
								m_running.Add(testItem);
							JobService.Enqueue(testItem.Item.Name, ExecuteScheduledItem, testItem);
							concBlocked.RemoveAt(i);
							i--;
						}
					}

					// then check all newly nodded items
					int numkicked = 0;
					for (int i = 0; i < numNodReleasedItems; i++)
					{
						var testItemIndex = nodReleasedItems[i];
						var testItem = items[testItemIndex];
						bool canRunNow = CanRunNow(testItem);
						if (canRunNow)
						{
							// no concurrency issue - run it!
							lock (m_running)
								m_running.Add(testItem);
							numkicked++;
							JobService.Enqueue(testItem.Item.Name, ExecuteScheduledItem, testItem);
						}
						else
						{
							concBlocked.Add(testItem);
						}
					}
				}
			}
		}

		// must be run under m_serial lock
		private bool CanRunNow(ItemInfo item)
		{
#if DEBUG
			CoreException.Assert(Monitor.IsEntered(m_serial));
#endif
			// check all running items for concurrency prohibitions
			var running = m_running;
			SymmetricMatrixBool concMatrix = m_concurrencyPreventionMatrix;
			lock (running)
			{
				for (int i = 0; i < running.Count; i++)
				{
					ref readonly var other = ref running[i];
					CoreException.Assert(other != item); // should be prevented by state not being Running

					// check for concurrency issue
					if (concMatrix[other.Index, item.Index] == true)
						return false;
				}
			}
			return true;
		}
	}

	public static class ScheduleOrderUtil
	{
		public static ScheduleOrder ResolveOrder(ScheduleOrder order, ScheduleOrder antiorder, out bool conflict)
		{
			//                                   A:RunBefore       A:AnyOrderConcurrent    A:AnyOrderNotConcurrent   A:RunAfter
			// B:RunBefore                       CONFLICT          RunAfter                RunAfter                   RunAfter
			// B:AnyOrderConcurrent              RunBefore         AnyOrderConcurrent      AnyOrderNotConcurrent     RunAfter
			// B:AnyOrderNotConcurrent           RunBefore         AnyOrderNotConcurrent   AnyOrderNotConcurrent     RunAfter
			// B:RunAfter                        RunBefore         RunBefore               RunBefore                 CONFLICT
			//
			conflict = false;
			switch (order)
			{
				case ScheduleOrder.AnyOrderConcurrent:
					if (antiorder == ScheduleOrder.RunAfter)
						return ScheduleOrder.RunBefore;
					if (antiorder == ScheduleOrder.RunBefore)
						return ScheduleOrder.RunAfter;
					return antiorder;

				case ScheduleOrder.AnyOrderNotConcurrent:
					if (antiorder == ScheduleOrder.RunAfter)
						return ScheduleOrder.RunBefore;
					if (antiorder == ScheduleOrder.RunBefore)
						return ScheduleOrder.RunAfter;
					return ScheduleOrder.AnyOrderNotConcurrent;

				case ScheduleOrder.RunBefore:
				case ScheduleOrder.RunAfter:
				default:
					if (antiorder == order)
						conflict = true;
					return order;
			}
		}
	}
}


<|start_filename|>src/DynamicOrdering.cs<|end_filename|>
﻿using System;

namespace Lidgren.Core
{
	public enum DynamicOrder
	{
		RequireABeforeB,
		PreferABeforeB,
		AnyOrder,
		PreferBBeforeA,
		RequireBBeforeA,
	}

	public interface IDynamicOrdered
	{
		DynamicOrder OrderAgainst(object other);
	}

	/// <summary>
	/// Sort ordering of items making individual ordering requests
	/// </summary>
	public static class DynamicOrdering
	{
		public static bool Perform<T>(Span<T> items, out string error) where T : IDynamicOrdered
		{
			int len = items.Length;

			var order = new FastList<int>(len);

			// first; determine number of dependencies for each item; to use as subscore when ordering
			var dependenciesPerItem = new int[len];
			for (int aidx = 0; aidx < len; aidx++)
			{
				for (int bidx = 0; bidx < len; bidx++)
				{
					if (aidx == bidx)
						continue;
					var cmp = GetOrder(items[aidx], items[bidx]);
					if (cmp == DynamicOrder.RequireABeforeB)
						dependenciesPerItem[bidx]++;
				}
			}

			for (int insertOriginalIndex = 0; insertOriginalIndex < len; insertOriginalIndex++)
			{
				if (order.Count == 0)
				{
					order.Add(insertOriginalIndex);
					continue;
				}

				ref readonly var item = ref items[insertOriginalIndex];

				var myDeps = dependenciesPerItem[insertOriginalIndex];

				// find best insertion point for this item
				int bestInsertionIndex = -1;
				int bestScore = -1;
				int bestPrecedingDeps = -1;

				//
				// test all insertion points; which is the best one
				//
				for (int testInsertIndex = 0; testInsertIndex < order.Count + 1; testInsertIndex++) // +1 to test insertion point efter all items
				{
					int score = 0;
					int numPrecedingDeps = 0;
					bool invalid = false;

					// score by those that would come before this insertion point
					for (int b = 0; b < testInsertIndex; b++)
					{
						int beforeIndex = order[b];
						var cmp = GetOrder(item, items[beforeIndex]);
						switch (cmp)
						{
							case DynamicOrder.RequireABeforeB:
								invalid = true;
								break;
							case DynamicOrder.AnyOrder:
								break;
							case DynamicOrder.PreferABeforeB:
								score--;
								break;
							case DynamicOrder.RequireBBeforeA:
							case DynamicOrder.PreferBBeforeA:
								score++;
								break;
						}
						numPrecedingDeps += dependenciesPerItem[beforeIndex];
					}
					if (invalid)
						continue;

					// check afters
					for (int a = testInsertIndex; a < order.Count; a++)
					{
						int afterIndex = order[a];
						var cmp = GetOrder(item, items[afterIndex]);
						switch (cmp)
						{
							case DynamicOrder.RequireBBeforeA:
								invalid = true;
								break;
							case DynamicOrder.PreferBBeforeA:
								score--;
								break;
							case DynamicOrder.PreferABeforeB:
							case DynamicOrder.RequireABeforeB:
								score++;
								break;
							case DynamicOrder.AnyOrder:
								break;
						}
						numPrecedingDeps += myDeps;
					}
					if (invalid)
						continue;

					if ((score > bestScore) || (score == bestScore && numPrecedingDeps < bestPrecedingDeps))
					{
						bestScore = score;
						bestPrecedingDeps = numPrecedingDeps;
						bestInsertionIndex = testInsertIndex;
					}
				}

				if (bestInsertionIndex == -1)
				{
					error = "Failed to insert " + item + " for dynamic ordering";
					return false;
				}

				order.Insert(bestInsertionIndex, insertOriginalIndex);

				//Console.WriteLine("Inserting " + items[insertOriginalIndex] + " at position " + bestInsertionIndex);
				//for (int i = 0; i < order.Count; i++)
				//	Console.WriteLine("  " + i + ": " + items[order[i]]);
			}

			// create ordered array
			T[] result = new T[items.Length];
			for (int i = 0; i < result.Length; i++)
				result[i] = items[order[i]];
			result.AsSpan().CopyTo(items);

			error = "";
			return true;
		}

		public static DynamicOrder GetOrder(IDynamicOrdered a, IDynamicOrdered b)
		{
			var AtoB = a.OrderAgainst(b);
			var BtoA = b.OrderAgainst(a);
			return Reconcile(AtoB, BtoA);
		}

		/// <summary>
		/// Returns dynamic order A -> B
		/// </summary>
		public static DynamicOrder Reconcile(DynamicOrder AtoB, DynamicOrder BtoA)
		{
			switch (AtoB)
			{
				case DynamicOrder.AnyOrder:
					return Reverse(BtoA);

				case DynamicOrder.RequireABeforeB:
					CoreException.Assert(BtoA != DynamicOrder.RequireABeforeB, "Inconsistent dynamic ordering");
					return AtoB;

				case DynamicOrder.RequireBBeforeA:
					CoreException.Assert(BtoA != DynamicOrder.RequireBBeforeA, "Inconsistent dynamic ordering");
					return AtoB;

				case DynamicOrder.PreferABeforeB:
					if (BtoA == DynamicOrder.PreferABeforeB)
						return DynamicOrder.AnyOrder; // clash in preferrence; but it's ok
					if (BtoA == DynamicOrder.RequireABeforeB)
						return DynamicOrder.RequireBBeforeA; // require takes precedence
					return AtoB;

				case DynamicOrder.PreferBBeforeA:
					if (BtoA == DynamicOrder.PreferBBeforeA)
						return DynamicOrder.AnyOrder; // clash in preferrence; but it's ok
					if (BtoA == DynamicOrder.RequireBBeforeA)
						return DynamicOrder.RequireABeforeB; // require takes precedence
					return AtoB;

				default:
					CoreException.ThrowNotImplemented();
					return DynamicOrder.AnyOrder;
			}
		}

		private static DynamicOrder Reverse(DynamicOrder cmp)
		{
			switch (cmp)
			{
				case DynamicOrder.RequireABeforeB:
					return DynamicOrder.RequireBBeforeA;
				case DynamicOrder.PreferABeforeB:
					return DynamicOrder.PreferBBeforeA;
				case DynamicOrder.AnyOrder:
					return DynamicOrder.AnyOrder;
				case DynamicOrder.PreferBBeforeA:
					return DynamicOrder.PreferABeforeB;
				case DynamicOrder.RequireBBeforeA:
					return DynamicOrder.RequireABeforeB;
				default:
					CoreException.ThrowNotImplemented();
					return DynamicOrder.AnyOrder;
			}
		}
	}
}


<|start_filename|>src/Collections/FastList/FastList.Access.cs<|end_filename|>
﻿using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Text;

namespace Lidgren.Core
{
	public partial class FastList<T>
	{
		public ref T this[int index]
		{
			[MethodImpl(MethodImplOptions.AggressiveInlining)]
			get => ref m_buffer[m_offset + index];
		}

		public ref T this[uint index]
		{
			[MethodImpl(MethodImplOptions.AggressiveInlining)]
			get => ref m_buffer[m_offset + index];
		}

		/// <summary>
		/// Creates a new array; use this.Items for in-place access
		/// </summary>
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public T[] ToArray()
		{
			if (m_count == 0)
				return System.Array.Empty<T>();
			return ReadOnlySpan.ToArray();
		}

		/// <summary>
		/// Returns and removes the last item in the list; returns false if list contains no items
		/// </summary>
		public bool TryPop(out T result)
		{
			var cnt = m_count;
			if (cnt == 0)
			{
				result = default;
				return false;
			}

			var index = m_offset + cnt - 1;
			result = m_buffer[index];
			m_buffer[index] = default;
			cnt--;
			if (cnt == 0)
				m_offset = 0;
			m_count = cnt;
			return true;
		}

		/// <summary>
		/// Returns and removes the last item in the list; throws if empty
		/// </summary>
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public T Pop()
		{
			var index = m_offset + m_count - 1;
			var result = m_buffer[index];
			m_buffer[index] = default;
			m_count--;
			return result;
		}

		/// <summary>
		/// If count > 0; removes first element in list, putting it in 'item' and returns true
		/// </summary>
		public bool TryDequeue(out T item)
		{
			if (m_count == 0)
			{
				item = default;
				return false;
			}
			item = m_buffer[m_offset];
			m_offset++;
			m_count--;
			if (m_count == 0)
				m_offset = 0;
			return true;
		}

		/// <summary>
		/// Copy items of list to destination; throws if destination too small
		/// </summary>
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void CopyTo(Span<T> destination)
		{
			ReadOnlySpan.CopyTo(destination);
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public bool Contains(T item)
		{
			return Array.IndexOf(m_buffer, item, m_offset, m_count) != -1;
			//return ReadOnlySpan.Contains(item);
		}
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public int IndexOf(in T item)
		{
			//if (item is IEquatable<T>)
			//	return ReadOnlySpan.IndexOf(item);
			//else
			return Array.IndexOf(m_buffer, item, m_offset, m_count) - m_offset;
		}
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public int LastIndexOf(in T item)
		{
			int off = m_offset;
			int cnt = m_count;

			return Array.LastIndexOf(m_buffer, item, m_offset + m_count - 1, m_count) - m_offset;
			//return ReadOnlySpan.LastIndexOf(item);
		}
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public bool IsEmpty() { return m_count == 0; }

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void Reverse()
		{
			if (m_count != 0)
				Array.Reverse<T>(m_buffer, m_offset, m_count);
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void Sort()
		{
#if NET5_0_OR_GREATER
			this.Span.Sort();
#else
			Array.Sort<T>(m_buffer, m_offset, m_count);
#endif
		}

#if NET5_0_OR_GREATER
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void Sort(Comparison<T> comparison)
		{
			this.Span.Sort(comparison);
		}
#endif

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public void Sort(IComparer<T> comparer)
		{
#if NET5_0_OR_GREATER
			this.Span.Sort(comparer);
#else
			Array.Sort<T>(m_buffer, m_offset, m_count, comparer);
#endif
		}

		/// <summary>
		/// Returns dictionary group by delegate
		/// </summary>
		public Dictionary<GROUPT, FastList<T>> GroupBy<GROUPT>(Func<T, GROUPT> getGroup)
		{
			var retval = new Dictionary<GROUPT, FastList<T>>();
			foreach (var item in ReadOnlySpan)
			{
				var g = getGroup(item);
				if (retval.TryGetValue(g, out var list) == false)
				{
					list = new FastList<T>();
					retval[g] = list;
				}
				list.Add(item);
			}
			return retval;
		}
	}
}


<|start_filename|>src/Text/WildcardMatcher.cs<|end_filename|>
﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;

namespace Lidgren.Core
{
	/// <summary>
	/// Reusable wildcard matcher supporting * (matching anything) and ? (matching any one character)
	/// </summary>
	public readonly struct WildcardMatcher
	{
		private enum MatcherType
		{
			Nothing,           // ""
			Everything,        // "*"
			ExactPattern,      // "abc"
			LeadingStar,       // "*abc"
			TrailingStar,      // "abc*"
			SingleStar,        // "a*b"
			EnclosingStars,    // "*abc*"
			QuestionMarks,     // "?b?"
			Tokens,            // every other combination
		}

		private enum TokenType
		{
			Exact = 0,
			QuestionMark = 2,
			Star = 3
		}

		[DebuggerDisplay("{Type} {Start} to {Start + Length - 1}")]
		private struct Token
		{
			public int Start;
			public int Length;
			public TokenType Type;
		}

		private readonly MatcherType m_matchType;
		private readonly string m_pattern;
		private readonly FastList<Token> m_tokens;
		private readonly StringComparison m_stringComparison;

		public WildcardMatcher(string pattern, bool ignoreCase = false)
		{
			m_pattern = pattern;
			m_tokens = null;
			m_stringComparison = ignoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal;

			if (pattern == null || pattern.Length == 0)
			{
				m_matchType = MatcherType.Nothing;
				return;
			}

			if (pattern.Equals("*", StringComparison.Ordinal))
			{
				m_matchType = MatcherType.Everything;
				return;
			}

			// find all tokens
			int numStars = 0;
			int curTokStart = 0;
			int curTokLen = 0;
			while(curTokStart + curTokLen < pattern.Length)
			{
				var examineIndex = curTokStart + curTokLen;
				var c = pattern[examineIndex];
				switch (c)
				{
					case '?':
					case '*':
						if (m_tokens == null)
							m_tokens = new FastList<Token>(8);

						// end current token
						if (curTokLen > 0)
						{
							ref var token = ref m_tokens.Add();
							token.Start = curTokStart;
							token.Length = curTokLen;
							token.Type = TokenType.Exact;
						}

						Token qtok;
						qtok.Start = examineIndex;
						qtok.Length = 1;

						if (c == '?')
						{
							qtok.Type = TokenType.QuestionMark;
							m_tokens.Add(qtok);
						}
						else
						{
							numStars++;
							qtok.Type = TokenType.Star;

							// remove consecutive stars
							var cnt = m_tokens.Count;
							if (cnt == 0 || m_tokens[cnt - 1].Type != TokenType.Star)
								m_tokens.Add(qtok); // else drop it
						}
						curTokStart = examineIndex + 1;
						curTokLen = 0;
						break;
					default:
						curTokLen++;
						break;
				}
			}

			if (curTokLen > 0)
			{
				// detect pattern without any wildcards
				if (m_tokens == null)
				{
					m_matchType = MatcherType.ExactPattern;
					return;
				}

				ref var token = ref m_tokens.Add();
				token.Start = curTokStart;
				token.Length = curTokLen;
				token.Type = TokenType.Exact;
			}

			// detect leading or trailing star
			if (m_tokens.Count == 2)
			{
				if (m_tokens[0].Type == TokenType.Star && m_tokens[1].Type == TokenType.Exact)
				{
					m_matchType = MatcherType.LeadingStar;
					return;
				}
				else if (m_tokens[0].Type == TokenType.Exact && m_tokens[1].Type == TokenType.Star)
				{
					m_matchType = MatcherType.TrailingStar;
					return;
				}
			}

			if (m_tokens.Count == 3)
			{
				// detect single sandwiched star
				if (m_tokens[0].Type == TokenType.Exact &&
				m_tokens[1].Type == TokenType.Star &&
				m_tokens[2].Type == TokenType.Exact)
				{
					m_matchType = MatcherType.SingleStar;
					return;
				}

				if (m_tokens[0].Type == TokenType.Star &&
				m_tokens[1].Type == TokenType.Exact &&
				m_tokens[2].Type == TokenType.Star)
				{
					// detect "contains"
					m_matchType = MatcherType.EnclosingStars;
					return;
				}
			}

			// complex token matching
			if (numStars == 0)
				m_matchType = MatcherType.QuestionMarks;
			else
				m_matchType = MatcherType.Tokens;
		}

		/// <summary>
		/// Returns true if input matches the pattern
		/// </summary>
		public readonly bool Matches(ReadOnlySpan<char> input)
		{
			if (m_matchType == MatcherType.Nothing)
				return input.Length == 0;
			if (m_matchType == MatcherType.Everything)
				return true;
			if (input.Length == 0)
				return false;

			var pattern = m_pattern.AsSpan();

			switch (m_matchType)
			{
				case MatcherType.ExactPattern:
					return input.Equals(pattern, m_stringComparison);
				case MatcherType.LeadingStar:
					return input.EndsWith(pattern.Slice(1), m_stringComparison);
				case MatcherType.TrailingStar:
					return input.StartsWith(pattern.Slice(0, pattern.Length - 1), m_stringComparison);
				case MatcherType.EnclosingStars:
					{
						ref readonly var secondToken = ref m_tokens[1];
						return input.Contains(pattern.Slice(secondToken.Start, secondToken.Length), m_stringComparison);
					}
				case MatcherType.QuestionMarks:
					{
						if (input.Length != pattern.Length)
							return false;
						if (m_stringComparison == StringComparison.Ordinal)
						{
							for (int i = 0; i < input.Length; i++)
							{
								var p = pattern[i];
								var c = input[i];
								if (p != c && p != '?')
									return false;
							}
						}
						else
						{
							for (int i = 0; i < input.Length; i++)
							{
								var p = char.ToLower(pattern[i]);
								var c = char.ToLower(input[i]);
								if (p != c && p != '?')
									return false;
							}
						}
						return true;
					}
				case MatcherType.SingleStar:
					{
						ref readonly var firstToken = ref m_tokens[0];
						CoreException.Assert(m_tokens[1].Type == TokenType.Star);
						ref readonly var lastToken = ref m_tokens[2];
						return input.StartsWith(pattern.Slice(0, firstToken.Length), m_stringComparison) &&
							input.EndsWith(pattern.Slice(lastToken.Start, lastToken.Length), m_stringComparison);
					}

				case MatcherType.Tokens:
					return MatchByTokens(input);
				default:
					CoreException.Throw("Unexpected MatcherType");
					return false;
			}
		}

		private readonly bool MatchByTokens(ReadOnlySpan<char> text)
		{
			var full = m_pattern.AsSpan();
			var tokens = m_tokens.ReadOnlySpan;

			while (tokens.Length > 0)
			{
				{
					ref readonly var firstToken = ref tokens[0];
					var firstTokenSlice = full.Slice(firstToken.Start, firstToken.Length);
					if (firstToken.Type == TokenType.Exact)
					{
						if (text.StartsWith(firstTokenSlice, m_stringComparison) == false)
							return false;
						text = text.Slice(firstToken.Length);
						tokens = tokens.Slice(1);
						continue;
					}
					if (firstToken.Type == TokenType.QuestionMark)
					{
						text = text.Slice(1);
						tokens = tokens.Slice(1);
						continue;
					}
				}

				{
					var lastTokenIndex = tokens.Length - 1;
					if (lastTokenIndex > 0)
					{
						ref readonly var lastToken = ref tokens[tokens.Length - 1];
						var lastTokenSlice = full.Slice(lastToken.Start, lastToken.Length);

						if (lastToken.Type == TokenType.Exact)
						{
							if (text.EndsWith(lastTokenSlice, m_stringComparison) == false)
								return false;
							text = text.Slice(0, text.Length - lastTokenSlice.Length);
							tokens = tokens.Slice(0, lastTokenIndex);
							continue;
						}
						if (lastToken.Type == TokenType.QuestionMark)
						{
							if (text.Length < 1)
								return false;
							text = text.Slice(0, text.Length - 1);
							tokens = tokens.Slice(0, lastTokenIndex);
							continue;
						}
					}
				}

				//
				// Ok, at this point FIRST and LAST tokens are STARS
				//

				CoreException.Assert(tokens[0].Type == TokenType.Star);
				CoreException.Assert(tokens.Length == 1 || tokens[tokens.Length - 1].Type == TokenType.Star);

				if (tokens.Length <= 2)
				{
					// only stars remaining!
					return true;
				}

				if (tokens.Length == 3)
				{
					// only one token in between; fast track
					ref readonly var midToken = ref tokens[1];
					switch (midToken.Type)
					{
						case TokenType.QuestionMark:
							return text.Length >= 1;
						case TokenType.Exact:
							return text.Contains(full.Slice(midToken.Start, midToken.Length), m_stringComparison);
						case TokenType.Star:
						default:
							CoreException.Throw("Unexpected token type");
							break;
					}
				}

				{
					ref readonly var firstToken = ref tokens[1];
					if (firstToken.Type == TokenType.Exact)
					{
						// narrow from start
						var idx = text.IndexOf(full.Slice(firstToken.Start, firstToken.Length), m_stringComparison);
						if (idx == -1)
							return false;

						tokens = tokens.Slice(2); // skip first star AND this token, since we've matched it
						text = text.Slice(idx + firstToken.Length);
						continue;
					}
				}

				{
					ref readonly var lastToken = ref tokens[tokens.Length - 2];
					if (lastToken.Type == TokenType.Exact)
					{
						// narrow from end
						var idx = text.LastIndexOf(full.Slice(lastToken.Start, lastToken.Length), m_stringComparison);
						if (idx == -1)
							return false;

						tokens = tokens.Slice(0, tokens.Length - 2); // skip last star AND this token, since we've matched it
						text = text.Slice(0, idx);
						continue;
					}
				}

				// ugh, we've got "*?...?*"
				CoreException.Assert(tokens[1].Type == TokenType.QuestionMark);
				CoreException.Assert(tokens[tokens.Length - 2].Type == TokenType.QuestionMark);

				// let's narrow from end
				if (text.Length < 2)
					return false; // can't possibly be two; gotta match at least two characters

				tokens = tokens.Slice(0, tokens.Length - 2);
				text = text.Slice(0, text.Length - 1);
				continue;
			}

			// out of tokens; success!
			return true;
		}
	}
}


<|start_filename|>tests/ExtensiveJobServiceTests.cs<|end_filename|>
﻿using System;
using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting;

using Lidgren.Core;
using System.Threading;

namespace UnitTests
{
	[TestClass]
	public class ExtensiveJobServiceTests
	{
		[TestMethod]
		public void TestJobServiceMore()
		{
			const float runFor = 4.0f; // run for this many seconds
			const int simultaneousTests = 10;

			JobService.Initialize();

			//TimingService.IsEnabled = true;
			//var chrome = new ChromeTraceTimingConsumer("ejobtests.json");

			var inProgress = new List<JobTest>();

			var end = TimeService.Wall + runFor;
			for(; ;)
			{
				double now = TimeService.Wall;
				if (now > end)
					break;

				// check for completeness and timeouts
				for (int i = 0; i < inProgress.Count; i++)
				{
					var test = inProgress[i];
					if (test.IsComplete())
					{
						test.Verify();
						inProgress.RemoveAt(i);
						i--;
					}
					else if (now > test.Timeout)
					{
						Assert.Fail(test.GetType().Name + " timed out");
						inProgress.RemoveAt(i);
						i--;
					}
				}

				// start tests if necessary
				while(inProgress.Count < simultaneousTests)
				{
					var test = StartRandomTest();
					inProgress.Add(test);
				}
			}

			for (int i = 0; i < m_runs.Length; i++)
			{
				if (m_runs[i] > 0)
					Console.WriteLine($"Test type {i} started {m_runs[i]} times");
			}

			//TimingService.TriggerFlush(true); // this will set TimingService.IsEnabled to false
			//chrome.Dispose();
		}

		private int[] m_runs = new int[10];

		private JobTest StartRandomTest()
		{
			JobTest test;
			var nr = PRNG.Next(0, 9);
			m_runs[nr]++;
			switch (nr)
			{
				case 0:
				case 1:
				case 2:
				case 3:
					test = new Simple();
					break;
				case 4:
				case 5:
					test = new Wide();
					break;
				case 6:
				case 7:
					test = new WideArg();
					break;
				case 8:
					test = new Delayed();
					break;
				default:
					throw new NotImplementedException();
			}
			test.Timeout = TimeService.Wall + test.Start();
			return test;
		}
	}

	public abstract class JobTest
	{
		protected string m_name;

		public JobTest()
		{
			m_name = GetType().Name + StringUtils.ToHex(PRNG.NextUInt32());
		}

		public double Timeout;
		public abstract float Start();
		public abstract bool IsComplete();
		public abstract void Verify();
	}

	public class Simple : JobTest
	{
		private bool m_didExecute = false;
		public override float Start()
		{
			JobService.Enqueue(m_name, Execute, m_name);
			return 1.0f;
		}

		public override bool IsComplete()
		{
			return m_didExecute;
		}

		private void Execute(object ob)
		{
			Assert.AreEqual(ob, m_name);
			Thread.Sleep(PRNG.Next(0, 4));
			m_didExecute = true;
		}

		public override void Verify()
		{
			Assert.IsTrue(m_didExecute);
		}
	}

	public class Wide : JobTest
	{
		private int m_ranTimes = 0;
		private int m_requireRuns;

		public override float Start()
		{
			m_ranTimes = 0;
			m_requireRuns = JobService.WorkersCount;
			JobService.EnqueueWide(m_name, Execute, m_name);
			return 1.0f;
		}

		private void Execute(object ob)
		{
			Assert.AreEqual(ob, m_name);
			Thread.Sleep(PRNG.Next(0, 4));
			Interlocked.Increment(ref m_ranTimes);
		}

		public override bool IsComplete()
		{
			return m_ranTimes >= m_requireRuns;
		}

		public override void Verify()
		{
			Assert.IsTrue(m_ranTimes == m_requireRuns);
		}
	}

	public class WideArg : JobTest
	{
		private bool m_allOk;
		private object[] m_args;
		private bool[] m_done;

		public override float Start()
		{
			var num = PRNG.Next(1, 5);
			m_args = new object[num];
			m_done = new bool[num];

			for (int i = 0; i < m_args.Length; i++)
				m_args[i] = StringUtils.ToHex(PRNG.NextUInt32());

			JobService.ForEachArgument(m_name, Execute, m_args, OnDone, "onDone");
			return 1.0f;
		}

		private void OnDone(object obj)
		{
			Assert.AreEqual("onDone", obj);

			Thread.Sleep(10);
			for (int i = 0; i < m_args.Length; i++)
				Assert.IsTrue(m_done[i]);
			m_allOk = true;
		}

		private void Execute(object ob)
		{
			// which
			for (int i = 0; i < m_args.Length; i++)
			{
				if (m_args[i] == ob)
				{
					Assert.IsFalse(m_done[i]);
					m_done[i] = true;
				}
			}
			Thread.Sleep(PRNG.Next(0, 4));
		}

		public override bool IsComplete()
		{
			return m_allOk;
		}

		public override void Verify()
		{
		}
	}

	public class Delayed : JobTest
	{
		private bool m_didExecute = false;
		private float m_delay;
		private double m_started;

		public override float Start()
		{
			m_started = TimeService.Wall;
			m_delay = PRNG.NextFloat(0.01f, 0.05f);
			JobService.Enqueue(m_name, Execute, m_name, m_delay);
			return m_delay + 1.0f;
		}

		public override bool IsComplete()
		{
			return m_didExecute;
		}

		private void Execute(object ob)
		{
			double ago = TimeService.Wall - m_started;
			Assert.IsTrue(ago > m_delay);
			Assert.IsTrue(ago < m_delay + 0.5);
			Assert.AreEqual(ob, m_name);
			Thread.Sleep(PRNG.Next(0, 4));
			m_didExecute = true;
		}

		public override void Verify()
		{
			Assert.IsTrue(m_didExecute);
		}
	}
}


<|start_filename|>src/Collections/Pool.cs<|end_filename|>
﻿using System;

namespace Lidgren.Core
{
	/// <summary>
	/// Thread safe pool of objects
	/// Example usage: new Pool<MyClass>(() => new MyClass(), 35);
	/// </summary>
	public class Pool<T> where T : class
	{
		public delegate T CreationDelegate();

		private readonly CreationDelegate m_create;

		private T[] m_items;
		private int m_maxPooledItems;

		private int m_itemsInPoolCount;
		public int Count { get { return m_itemsInPoolCount; } }

		public Pool(CreationDelegate creator, int maxPoolItems)
		{
			m_items = new T[maxPoolItems < 32 ? maxPoolItems : 32];
			m_maxPooledItems = maxPoolItems;
			m_create = creator;
		}

		/// <summary>
		/// Clears pool and all references to the objects in it
		/// </summary>
		public void Clear()
		{
			lock (m_create)
			{
				m_items.AsSpan().Clear();
				m_itemsInPoolCount = 0;
			}
		}

		/// <summary>
		/// Returns an item from the pool if available, otherwise creates one and returns it
		/// </summary>
		public T Get()
		{
			lock (m_create)
			{
				var n = m_itemsInPoolCount;
				if (n > 0)
				{
					n--;
					var retval = m_items[n];
					m_items[n] = default;
					m_itemsInPoolCount = n;
					return retval;
				}
			}

			// no item in pool
			return m_create();
		}

		/// <summary>
		/// Returns true if item was returned to pool; false if the pool was full and not added
		/// </summary>
		public bool Return(T item)
		{
			lock (m_create)
			{
				var n = m_itemsInPoolCount;
				if (n == m_maxPooledItems)
					return false; // pool is full

				int curStoreSize = m_items.Length;
				if (n >= curStoreSize)
				{
					// grow
					var newItems = new T[Math.Min(curStoreSize * 2, m_maxPooledItems)];
					m_items.AsSpan().CopyTo(newItems);
					m_items = newItems;
				}
				m_items[n] = item;
				m_itemsInPoolCount = n + 1;
				return true;
			}
		}
	}
}


<|start_filename|>tests/StringUtilsTests.cs<|end_filename|>
﻿using System;
using System.Text;
using Lidgren.Core;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace UnitTests
{
	[TestClass]
	public partial class StringUtilsTests
	{
		[TestMethod]
		public void TestStringUtils()
		{
			Assert.AreEqual(11, StringUtils.HexCharToInteger('B'));

			var cbuf = new char[128];

			var testNumbers = new ulong[]
			{
				0, 1, 2, 100, 1000, 10000, 100000, 10000000,
				(ulong)PRNG.Next(0, 10000),
				(ulong)PRNG.Next(0, 10000),
				(ulong)PRNG.Next(0, 10000),
				(ulong)PRNG.Next(0, 10000),
				(ulong)PRNG.Next(0, 10000),
				(ulong)PRNG.Next(0, 10000),
				(ulong)PRNG.Next(0, 10000)
			};

			foreach(var val in testNumbers)
			{
				var sans = val.ToString("X");
				var str = (PRNG.NextBool() ? "0x" : "") + sans;

				Assert.AreEqual(val, StringUtils.FromHex(str));

				var utfBytes = Encoding.UTF8.GetBytes(str);
				Assert.AreEqual(val, StringUtils.FromUTF8Hex(utfBytes));

				var cnt = StringUtils.ToHex(val, cbuf);
				Assert.AreEqual(sans.Length, cnt);
				Assert.IsTrue(sans.AsSpan().SequenceEqual(cbuf.AsSpan(0, cnt)));
			}

			var arr = new byte[64];
			PRNG.NextBytes(arr);

			var facit = BitConverter.ToString(arr).Replace("-", "");
			var my = StringUtils.ToHex(arr);
			Assert.AreEqual(facit, my);

			var dvalstr = "0.00000000000123";
			var dval = double.Parse(dvalstr, System.Globalization.CultureInfo.InvariantCulture);
			var lots = StringUtils.DoubleToString(dval);

			// this is "1.23E-12"
			var defaultToString = dval.ToString(System.Globalization.CultureInfo.InvariantCulture);

			Assert.AreEqual(dvalstr, StringUtils.DoubleToString(dval));
		}
	}
}


<|start_filename|>src/TimeService.cs<|end_filename|>
﻿#nullable enable
using System;
using System.Diagnostics;
using System.Globalization;
using System.Runtime.CompilerServices;

namespace Lidgren.Core
{
	public static class TimeService
	{
		private readonly static long s_timeInitialized;
		private readonly static double s_dFreq;
		private readonly static int s_iFreq;
		private readonly static double s_dInvFreq;
		private readonly static double s_dInvMilliFreq;
		private readonly static double s_epochOffset;

		private readonly static DateTime s_epoch;

		/// <summary>
		/// Wall time in seconds since TimeService was initialized
		/// </summary>
		public static double Wall { get { return (double)(Stopwatch.GetTimestamp() - s_timeInitialized) * s_dInvFreq; } }

		public static int TicksPerSecond { get { return s_iFreq; } }

		/// <summary>
		/// Seconds since 1970-01-01 00:00
		/// </summary>
		public static double EpochWall { get { return Wall + s_epochOffset; } }

		/// <summary>
		/// Convert from time produced by TimeService.Wall to corresponding Stopwatch.GetTimestamp ticks
		/// </summary>
		public static long WallToTicks(double wall)
		{
			return s_timeInitialized + (long)(wall * s_dFreq);
		}

		/// <summary>
		/// Convert duration in seconds to duration in ticks
		/// </summary>
		public static long SecondsToTicks(double seconds)
		{
			return (long)(seconds * s_dFreq);
		}

		static TimeService()
		{
			s_timeInitialized = Stopwatch.GetTimestamp();
			var freq = Stopwatch.Frequency;
			s_dFreq = (double)freq;
			s_iFreq = (int)freq;
			s_dInvFreq = 1.0 / s_dFreq;
			s_dInvMilliFreq = 1000.0 * s_dInvFreq;
			s_epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
			s_epochOffset = (DateTime.UtcNow - s_epoch).TotalSeconds;
		}

		public static string CompactDuration(double seconds)
		{
			if (seconds <= 0.1)
				return String.Format(CultureInfo.InvariantCulture, "{0:0.#}", seconds * 1000) + "ms";

			if (seconds < 1.0)
				return (int)(seconds * 1000) + "ms";

			if (seconds < 60.0)
				return String.Format(CultureInfo.InvariantCulture, "{0:0.##}", seconds) + "s";

			// 1m to <60m
			if (seconds < 60 * 60)
			{
				int mins = (int)(seconds / 60);
				seconds -= (mins * 60);
				if (seconds == 0)
					return string.Format(CultureInfo.InvariantCulture, "{0}m", mins);
				return string.Format(CultureInfo.InvariantCulture, "{0}m{1:0.##}s", mins, seconds);
			}

			{
				const double secondsInHour = 60 * 60;
				int hrs = (int)(seconds / secondsInHour);
				seconds -= (hrs * secondsInHour);
				if (seconds == 0)
					return string.Format(CultureInfo.InvariantCulture, "{0}h", hrs);
				int mins = (int)(seconds / 60);
				seconds -= (mins * 60);
				if (seconds == 0)
					return string.Format(CultureInfo.InvariantCulture, "{0}h{1}m", hrs, mins);

				return string.Format(CultureInfo.InvariantCulture, "{0}h{1}m{2:0.##}s", hrs, mins, seconds);
			}
		}

		public static string TicksToCompactDuration(long tickCount)
		{
			return CompactDuration((double)(tickCount * s_dInvFreq));
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static double TicksToSeconds(long tickCount)
		{
			return (double)tickCount * s_dInvFreq;
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static double TicksToMilliSeconds(long stopwatchTicks)
		{
			return (double)stopwatchTicks * s_dInvMilliFreq;
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static string Measure(long stopwatchTicksStarted)
		{
			var end = Stopwatch.GetTimestamp();
			var dur = end - stopwatchTicksStarted;
			return CompactDuration((double)dur * s_dInvFreq);
		}

		/// <summary>
		/// Parse "1d8h4m10s100ms" to number of seconds
		/// </summary>
		public static bool TryParseDuration(string str, out double seconds)
		{
			seconds = 0.0;

			// cover all-digits case
			bool isAllDigits = true;
			foreach (var c in str)
			{
				if (char.IsDigit(c) == false)
				{
					isAllDigits = false;
					break;
				}
			}
			if (isAllDigits)
				return double.TryParse(str, out seconds);

			int idx = 0;
			while (idx < str.Length)
			{
				// get number
				int n = idx;
				for(; ;)
				{
					if (n >= str.Length)
						break;
					var c = str[n];
					if (char.IsDigit(c) == false && c != '.' && c != '-')
						break;
					n++;
				}
				if (n == idx || n >= str.Length)
					return false;

				double amount;
				if (double.TryParse(str.AsSpan(idx, n - idx), NumberStyles.Number, CultureInfo.InvariantCulture, out amount) == false)
					return false;

				// get unit
				double multiplier;
				switch (str[n])
				{
					case 's':
						multiplier = 1.0;
						idx = n + 1; // skip unit
						break;
					case 'm':
						// minutes or milliseconds?
						if (str.Length > n + 1 && str[n + 1] == 's')
						{
							multiplier = 0.001;
							idx = n + 2; // skip unit
						}
						else
						{
							multiplier = 60.0;
							idx = n + 1; // skip unit
						}
						break;
					case 'h':
						multiplier = (60.0 * 60.0);
						idx = n + 1; // skip unit
						break;
					case 'd':
						multiplier = (60.0 * 60.0 * 24.0);
						idx = n + 1; // skip unit
						break;
					default:
						return false;
				}

				amount *= multiplier;
				seconds += amount;
			}
			return true;
		}

		/// <summary>
		/// Seconds since 1970-01-01
		/// </summary>
		public static double ToEpochSeconds(DateTime? time)
		{
			if (time.HasValue == false)
				return 0;
			var t = time.Value;
			if (t.Kind == DateTimeKind.Utc)
				return (t - s_epoch).TotalSeconds;
			var ut = t.ToUniversalTime();
			return (ut - s_epoch).TotalSeconds;
		}

		/// <summary>
		/// Pass seconds since 1970-01-01
		/// </summary>
		public static DateTime FromEpochSeconds(double seconds)
		{
			if (seconds >= TimeSpan.MaxValue.TotalSeconds)
				return DateTime.MaxValue;
			return s_epoch + TimeSpan.FromSeconds(seconds);
		}
	}
}
