diff --git a/src/BitSet/BitInfo.cs b/src/BitSet/BitInfo.cs new file mode 100644 index 0000000..5d6e8ca --- /dev/null +++ b/src/BitSet/BitInfo.cs @@ -0,0 +1,29 @@ +using System.Runtime.CompilerServices; + +namespace BitSet +{ + public static class BitInfo + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static sbyte GetFirstBitIndex(ulong mask) + { + for (sbyte i = 0; i < 64; i++) + { + if ((mask & (1UL << i)) > 0) + return i; + } + + return -1; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static sbyte GetFirstBitIndex(long mask) + { + return GetFirstBitIndex((ulong)mask); + } + + public static ulong BitsToBytes(ulong bits) + { + return (bits + 7) >> 3; + } + } +} diff --git a/src/BitSet/BitSet.csproj b/src/BitSet/BitSet.csproj new file mode 100644 index 0000000..1bd40e7 --- /dev/null +++ b/src/BitSet/BitSet.csproj @@ -0,0 +1,79 @@ + + + + + Debug + AnyCPU + {8F8A6380-5B10-4891-AC21-3D03494B3E44} + Library + Properties + BitSet + BitSet + v4.5.2 + true + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + true + true + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + true + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/BitSet/BitStream.cs b/src/BitSet/BitStream.cs new file mode 100644 index 0000000..1366198 --- /dev/null +++ b/src/BitSet/BitStream.cs @@ -0,0 +1,299 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BitSet +{ + [DebuggerDisplay("{ToString(),nq}")] + public class BitStream : ICloneable + { + public ulong Length + { + get { return m_MaxCursor - m_MinCursor; } + } + + public ulong Cursor + { + get { return m_Cursor - m_MinCursor; } + set + { + ulong newCursor = m_MinCursor + value; + if (newCursor > m_MaxCursor) + throw new OverflowException(string.Format("Attempted seek beyond the bounds of this {0}", nameof(BitStream))); + + m_Cursor = newCursor; + } + } + + byte[] m_Data; + ulong m_Cursor; + + ulong m_MinCursor; + ulong m_MaxCursor; + + private IEnumerable> DebugBitsGrouped + { + get + { + ulong count = 0; + ulong total = 0; + bool old = false; + foreach (bool bit in DebugBits) + { + if (bit != old && count > 0) + { + yield return Tuple.Create(count, old, total); + count = 0; + } + + old = bit; + count++; + total++; + } + } + } + + private IEnumerable DebugBits + { + get + { + for (ulong i = m_Cursor; i < m_MaxCursor; i++) + { + ulong dummy = i; + yield return BitReader.ReadBool(m_Data, ref dummy); + } + } + } + + private BitStream() { } + + public BitStream(byte[] data, ulong? cursor = null, ulong minCursor = 0, ulong? maxCursor = null) + { + if (data == null) + throw new ArgumentNullException(nameof(data)); + + if (!maxCursor.HasValue) + maxCursor = (ulong)data.LongLength * 8; + + if (minCursor > maxCursor.Value) + throw new ArgumentOutOfRangeException(nameof(minCursor), string.Format("{0} ({1}) greater than {2} ({3})", + nameof(minCursor), minCursor, nameof(maxCursor), maxCursor)); + + if (!cursor.HasValue) + cursor = minCursor; + + if (cursor.Value < minCursor || cursor.Value > maxCursor.Value) + throw new ArgumentOutOfRangeException(nameof(cursor), string.Format("{0} ({1}) outside the range specified by {2} and {3} [{4}, {5}]", nameof(cursor), cursor, nameof(minCursor), nameof(maxCursor), minCursor, maxCursor)); + + m_Data = data; + m_Cursor = cursor.Value; + m_MinCursor = minCursor; + m_MaxCursor = maxCursor.Value; + } + + public ulong ReadULong(byte bits = 64) + { + if (bits < 1 || bits > 64) + throw new ArgumentOutOfRangeException(nameof(bits)); + + ThrowIfOverflow(bits); + + return BitReader.ReadUIntBits(m_Data, ref m_Cursor, bits); + } + + public short ReadShort(byte bits = 16) + { + return (short)(ReadUShort(bits) << (32 - bits) >> (32 - bits)); + } + public ushort ReadUShort(byte bits = 16) + { + if (bits < 1 || bits > 16) + throw new ArgumentOutOfRangeException(nameof(bits)); + + ThrowIfOverflow(bits); + + return (ushort)BitReader.ReadUIntBits(m_Data, ref m_Cursor, bits); + } + + public uint ReadUInt(byte bits = 32) + { + if (bits < 1 || bits > 32) + throw new ArgumentOutOfRangeException(nameof(bits)); + + ThrowIfOverflow(bits); + + return (uint)BitReader.ReadUIntBits(m_Data, ref m_Cursor, bits); + } + public int ReadInt(byte bits = 32) + { + return unchecked((int)ReadUInt(bits)) << (32 - bits) >> (32 - bits); + } + + public byte ReadByte(byte bits = 8) + { + if (bits < 1 || bits > 8) + throw new ArgumentOutOfRangeException(nameof(bits)); + + ThrowIfOverflow(bits); + + return (byte)BitReader.ReadUIntBits(m_Data, ref m_Cursor, bits); + } + + public byte[] ReadBytes(ulong bytes) + { + ThrowIfOverflow(bytes * 8); + + byte[] retVal = new byte[bytes]; + + for (ulong i = 0; i < bytes; i++) + retVal[i] = ReadByte(8); + + return retVal; + } + + public float ReadSingle() + { + ThrowIfOverflow(32); + + return BitReader.ReadSingle(m_Data, ref m_Cursor); + } + + public bool PeekBool() + { + ThrowIfOverflow(1); + + ulong dummy = m_Cursor; + return BitReader.ReadUIntBits(m_Data, ref dummy, 1) == 1; + } + public bool ReadBool() + { + ThrowIfOverflow(1); + + return BitReader.ReadUIntBits(m_Data, ref m_Cursor, 1) == 1; + } + + public string ReadCString() + { + ulong startCursor = m_Cursor; + + string retVal = BitReader.ReadCString(m_Data, ref m_Cursor); + + ulong endCursor = m_Cursor; + m_Cursor = startCursor; + + ThrowIfOverflow(endCursor - startCursor); + + m_Cursor = endCursor; + + Debug.Assert(retVal != null); + return retVal; + } + + public char ReadChar() + { + ThrowIfOverflow(8); + + return Encoding.ASCII.GetChars(new byte[1] { ReadByte() }).Single(); + } + + public uint ReadVarUInt() + { + ulong startCursor = m_Cursor; + + uint retVal = BitReader.ReadVarInt(m_Data, ref m_Cursor); + + ulong endCursor = m_Cursor; + m_Cursor = startCursor; + + ThrowIfOverflow(endCursor - startCursor); + + m_Cursor = endCursor; + + return retVal; + } + + public int ReadVarInt() + { + var result = ReadVarUInt(); + return (int)((result >> 1) ^ -(result & 1)); + } + + public bool CheckOverflow(ulong bits) + { + if ((m_Cursor + bits) > m_MaxCursor) + return true; + else + return false; + } + + void ThrowIfOverflow(ulong bits) + { + if (CheckOverflow(bits)) + throw new OverflowException(string.Format("Attempted seek beyond the bounds of this {0}", nameof(BitStream))); + } + + public BitStream Subsection(ulong minCursor = 0, ulong? maxCursor = null) + { + if (!maxCursor.HasValue) + maxCursor = m_MaxCursor; + + if (minCursor > maxCursor.Value) + throw new ArgumentOutOfRangeException(nameof(minCursor), string.Format("{0} ({1}) greater than {2} ({3})", nameof(minCursor), minCursor, nameof(maxCursor), maxCursor)); + + if (maxCursor.Value > m_MaxCursor) + throw new ArgumentOutOfRangeException(nameof(maxCursor), string.Format("{0} ({1}) greater than {2} ({3})", nameof(maxCursor), maxCursor, nameof(Length), Length)); + + BitStream retVal = new BitStream(); + retVal.m_Data = m_Data; + + retVal.m_MinCursor = m_MinCursor + minCursor; + + if (maxCursor.HasValue) + retVal.m_MaxCursor = m_MinCursor + maxCursor.Value; + else + retVal.m_MaxCursor = m_MaxCursor; + + retVal.m_Cursor = retVal.m_MinCursor; + + return retVal; + } + + public ulong Seek(ulong bits, SeekOrigin origin) + { + if (origin == SeekOrigin.Begin) + Cursor = bits; + else if (origin == SeekOrigin.Current) + Cursor += bits; + else if (origin == SeekOrigin.End) + Cursor = Length - bits; + + return Length; + } + public ulong Seek(long bits, SeekOrigin origin) + { + if (origin == SeekOrigin.Begin) + Cursor = (ulong)bits; + else if (origin == SeekOrigin.Current) + Cursor = (ulong)((long)Cursor + bits); + else if (origin == SeekOrigin.End) + Cursor = (ulong)((long)Length - bits); + + return Cursor; + } + + public BitStream Clone() + { + return (BitStream)MemberwiseClone(); + } + object ICloneable.Clone() { return Clone(); } + + public override string ToString() + { + return string.Format("{0}: {1} / {2}", nameof(BitStream), Cursor, Length); + } + } +} diff --git a/src/BitSet/CString.cs b/src/BitSet/CString.cs new file mode 100644 index 0000000..e9b8edd --- /dev/null +++ b/src/BitSet/CString.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BitSet +{ + public static partial class BitReader + { + public static string ReadCString(byte[] buffer, ref ulong bitOffset) + { + StringBuilder builder = new StringBuilder(); + + char c; + while ((c = (char)ReadUIntBits(buffer, ref bitOffset, 8)) != '\0') + { + builder.Append(c); + } + + return builder.ToString(); + } + } +} diff --git a/src/BitSet/CopyBits.cs b/src/BitSet/CopyBits.cs new file mode 100644 index 0000000..0ca8227 --- /dev/null +++ b/src/BitSet/CopyBits.cs @@ -0,0 +1,137 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace BitSet +{ + public static partial class BitReader + { + [DllImport("kernel32.dll", EntryPoint = "CopyMemory", SetLastError = false)] + private static extern void CopyMemory(IntPtr dest, IntPtr src, uint count); + + public static void CopyBits(byte[] src, ulong bitsToCopy, ref ulong readBitOffset, byte[] dest) + { + ulong dummy = 0; + CopyBits(src, bitsToCopy, ref readBitOffset, dest, ref dummy); + } + + public static void CopyBits(byte[] src, ulong bitsToCopy, ref ulong readBitOffset, + byte[] dest, ref ulong writeBitOffset) + { + var copied = CopyBits(src, bitsToCopy, (byte)(readBitOffset % 8), readBitOffset / 8, + dest, (byte)(writeBitOffset % 8), writeBitOffset / 8); + + readBitOffset += copied; + writeBitOffset += copied; + } + + public static unsafe ulong CopyBits(byte[] src, ulong bitsToCopy, byte readBitOffset, ulong readByteOffset, + byte[] dest, byte writeBitOffset = 0, ulong writeByteOffset = 0) + { + if ((bitsToCopy % 8) == 0 && readBitOffset == 0 && writeBitOffset == 0) + { + // We're perfectly aligned, so we can just copy straight over. + Array.ConstrainedCopy(src, (int)readByteOffset, dest, (int)writeByteOffset, (int)(bitsToCopy / 8)); + return bitsToCopy; + } + + fixed (void* voidSrc = src, voidDst = dest) + { + var bitsToCopyOriginal = bitsToCopy; + + while (bitsToCopy > 0) + { + ulong buffer = 0; + byte bytesToRead = (byte)Math.Min(BitInfo.BitsToBytes(bitsToCopy + readBitOffset), 8); + + CopyMemory(new IntPtr(&((byte*)&buffer)[0]), new IntPtr(&((byte*)voidSrc)[readByteOffset]), bytesToRead); + //memcpy(&((byte*)&buffer)[0], &((byte*)voidSrc)[readByteOffset], bytesToRead); + + // Cut off any high bits we don't want + ulong readMask = (0xFFFFFFFFFFFFFFFF << readBitOffset) & + (0xFFFFFFFFFFFFFFFF >> (int)(64 - Math.Min(readBitOffset + bitsToCopy, 64))); + + buffer &= readMask; + + // Shift everything right so the first read bit is actually at bit 0 + buffer = (buffer >> readBitOffset); + + buffer = (buffer << writeBitOffset); + + ulong writeMask = + (0xFFFFFFFFFFFFFFFF << writeBitOffset) & + (0xFFFFFFFFFFFFFFFF >> (int)(64 - Math.Min(writeBitOffset + bitsToCopy, 64))); + + unchecked + { + if ((bitsToCopy + writeBitOffset) > 56) // >= 8 bytes available + { + // Write 8 bytes. + + // Clear bits to 0 first + *(ulong*)(&(((byte*)voidDst)[writeByteOffset])) &= ~writeMask; + + // Now write the data + *(ulong*)(&(((byte*)voidDst)[writeByteOffset])) |= (buffer & writeMask); + } + else if ((bitsToCopy + writeBitOffset) > 24) // >= 4 bytes available + { + // Write byte 1 through 4 + *(uint*)(&(((byte*)voidDst)[writeByteOffset])) &= ~(uint)writeMask; + *(uint*)(&(((byte*)voidDst)[writeByteOffset])) |= (uint)(buffer & writeMask); + + if ((bitsToCopy + writeBitOffset) > 40) // >= 6 bytes available + { + // Write byte 5 and 6 + *(ushort*)(&(((byte*)voidDst)[writeByteOffset + 4])) &= (ushort)~(writeMask >> 4 * 8); + *(ushort*)(&(((byte*)voidDst)[writeByteOffset + 4])) |= (ushort)((buffer >> 4 * 8) & (writeMask >> 4 * 8)); + } + else if ((bitsToCopy + writeBitOffset) > 32) // >= 5 bytes available + { + // Write byte 5 + *(&(((byte*)voidDst)[writeByteOffset + 4])) &= (byte)~(writeMask >> 4 * 8); + *(&(((byte*)voidDst)[writeByteOffset + 4])) |= (byte)((buffer >> 4 * 8) & (writeMask >> 4 * 8)); + } + + if ((bitsToCopy + writeBitOffset) > 48) // >= 7 bytes available + { + // Write byte 7 + *(&(((byte*)voidDst)[writeByteOffset + 6])) &= (byte)~(writeMask >> 6 * 8); + *(&(((byte*)voidDst)[writeByteOffset + 6])) |= (byte)((buffer >> 6 * 8) & (writeMask >> 6 * 8)); + } + } + else if ((bitsToCopy + writeBitOffset) > 8) // >= 2 bytes available + { + // Write byte 1 and 2 + *(ushort*)(&(((byte*)voidDst)[writeByteOffset])) &= (ushort)~writeMask; + *(ushort*)(&(((byte*)voidDst)[writeByteOffset])) |= (ushort)(buffer & writeMask); + + if ((bitsToCopy + writeBitOffset) > 16) // >= 3 bytes available + { + // Write byte 3 + *(&(((byte*)voidDst)[writeByteOffset + 2])) &= (byte)~(writeMask >> 2 * 8); + *(&(((byte*)voidDst)[writeByteOffset + 2])) |= (byte)((buffer >> 2 * 8) & (writeMask >> 2 * 8)); + } + } + else // >= 1 byte available + { + // Write byte 1 + *(&(((byte*)voidDst)[writeByteOffset])) &= (byte)~writeMask; + *(&(((byte*)voidDst)[writeByteOffset])) |= (byte)(buffer & writeMask); + } + } + + byte bytesRead = Math.Min(bytesToRead, (byte)7); + bitsToCopy -= Math.Min(bitsToCopy, (ulong)(bytesRead * 8)); + readByteOffset += bytesRead; + writeByteOffset += bytesRead; + } + + return bitsToCopyOriginal; + } + } + } +} diff --git a/src/BitSet/Single.cs b/src/BitSet/Single.cs new file mode 100644 index 0000000..1fec6f8 --- /dev/null +++ b/src/BitSet/Single.cs @@ -0,0 +1,72 @@ +using System; +using System.Runtime.CompilerServices; + +namespace BitSet +{ + public static partial class BitReader + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float ReadSingle(byte[] buffer, int startByte = 0) + { + throw new NotImplementedException(); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float ReadSingle(byte[] buffer, int startByte, byte bitOffset) + { + throw new NotImplementedException(); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe static float ReadSingle(byte* buffer, int startByte = 0) + { + return *(float*)(&buffer[startByte]); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe static float ReadSingle(byte* buffer, int startByte, byte bitOffset) + { + throw new NotImplementedException(); + } + } + public static partial class BitWriter + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteSingle(float value, byte[] buffer, int startByte = 0) + { + throw new NotImplementedException(); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteSingle(float value, byte[] buffer, int startByte, byte bitOffset) + { + throw new NotImplementedException(); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe static void WriteSingle(float value, byte* buffer, int startByte = 0) + { + *(float*)(&buffer[startByte]) = value; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe static void WriteSingle(float value, byte* buffer, int startByte, byte bitOffset) + { + throw new NotImplementedException(); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteSingle(double value, byte[] buffer, int startByte = 0) + { + WriteSingle((float)value, buffer, startByte); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteSingle(double value, byte[] buffer, int startByte, byte bitOffset) + { + WriteSingle((float)value, buffer, startByte, bitOffset); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe static void WriteSingle(double value, byte* buffer, int startByte = 0) + { + WriteSingle((float)value, buffer, startByte); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe static void WriteSingle(double value, byte* buffer, int startByte, byte bitOffset) + { + WriteSingle((float)value, buffer, startByte, bitOffset); + } + } +} diff --git a/src/BitSet/UInt.cs b/src/BitSet/UInt.cs new file mode 100644 index 0000000..67cce3a --- /dev/null +++ b/src/BitSet/UInt.cs @@ -0,0 +1,76 @@ +using System; +using System.Linq; +using System.Text; + +namespace BitSet +{ + public static partial class BitReader + { + public static ulong ReadUIntBits(byte[] buffer, ref ulong bitOffset, byte bits) + { +#if false + switch (bits) + { + case 1: return ReadUInt1(buffer, ref bitOffset); + case 2: return ReadUInt2(buffer, ref bitOffset); + case 3: return ReadUInt3(buffer, ref bitOffset); + case 4: return ReadUInt4(buffer, ref bitOffset); + case 5: return ReadUInt5(buffer, ref bitOffset); + case 6: return ReadUInt6(buffer, ref bitOffset); + + case 16: return ReadUInt16(buffer, ref bitOffset); + + case 20: return ReadUInt20(buffer, ref bitOffset); + + case 32: return ReadUInt32(buffer, ref bitOffset); + + case 48: return ReadUInt48(buffer, ref bitOffset); + + default: throw new NotImplementedException(); + } +#endif + + byte[] temp = new byte[8]; + CopyBits(buffer, bits, ref bitOffset, temp); + return BitConverter.ToUInt64(temp, 0); + } + + public static float ReadSingle(byte[] buffer, ref ulong bitOffset) + { + return BitConverter.ToSingle(BitConverter.GetBytes(ReadUInt(buffer, ref bitOffset)), 0); + } + + public static ulong ReadULong(byte[] buffer, ref ulong bitOffset) + { + return ReadUIntBits(buffer, ref bitOffset, sizeof(ulong) << 3); + } + public static uint ReadUInt(byte[] buffer, ref ulong bitOffset) + { + return (uint)ReadUIntBits(buffer, ref bitOffset, sizeof(uint) << 3); + } + public static int ReadInt(byte[] buffer, ref ulong bitOffset) + { + return unchecked((int)ReadUIntBits(buffer, ref bitOffset, sizeof(int) << 3)); + } + public static ushort ReadUShort(byte[] buffer, ref ulong bitOffset) + { + return (ushort)ReadUIntBits(buffer, ref bitOffset, sizeof(ushort) << 3); + } + public static short ReadShort(byte[] buffer, ref ulong bitOffset) + { + return unchecked((short)ReadUIntBits(buffer, ref bitOffset, sizeof(short) << 3)); + } + public static char ReadChar(byte[] buffer, ref ulong bitOffset) + { + return Encoding.ASCII.GetChars(new byte[] { ReadByte(buffer, ref bitOffset) }).Single(); + } + public static byte ReadByte(byte[] buffer, ref ulong bitOffset) + { + return (byte)ReadUIntBits(buffer, ref bitOffset, sizeof(byte) << 3); + } + public static bool ReadBool(byte[] buffer, ref ulong bitOffset) + { + return ReadUIntBits(buffer, ref bitOffset, 1) != 0; + } + } +} diff --git a/src/BitSet/UInt1.cs b/src/BitSet/UInt1.cs new file mode 100644 index 0000000..2116286 --- /dev/null +++ b/src/BitSet/UInt1.cs @@ -0,0 +1,72 @@ +using System; +using System.Runtime.CompilerServices; + +namespace BitSet +{ + public static partial class BitReader + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte ReadUInt1(byte[] buffer, ref ulong startBit) + { + var retVal = ReadUInt1(buffer, startBit); + startBit++; + return retVal; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte ReadUInt1(byte[] buffer, ulong startBit) + { + return ReadUInt1(buffer, (int)(startBit / 8), (byte)(startBit % 8)); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte ReadUInt1(byte[] buffer, int startByte = 0) + { + throw new NotImplementedException(); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte ReadUInt1(byte[] buffer, int startByte, byte bitOffset) + { + if (bitOffset > 7) + throw new ArgumentOutOfRangeException(nameof(bitOffset)); + + return (byte)(((buffer[startByte] & (1 << bitOffset)) != 0) ? 1 : 0); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe static byte ReadUInt1(byte* buffer, ulong startBit) + { + return ReadUInt1(buffer, (int)(startBit / 8), (byte)(startBit % 8)); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe static byte ReadUInt1(byte* buffer, int startByte = 0) + { + throw new NotImplementedException(); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe static byte ReadUInt1(byte* buffer, int startByte, byte bitOffset) + { + throw new NotImplementedException(); + } + } + public static partial class BitWriter + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteUInt1(byte value, byte[] buffer, int startByte = 0) + { + throw new NotImplementedException(); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteUInt1(byte value, byte[] buffer, int startByte, byte bitOffset) + { + throw new NotImplementedException(); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe static void WriteUInt1(byte value, byte* buffer, int startByte = 0) + { + throw new NotImplementedException(); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe static void WriteUInt1(byte value, byte* buffer, int startByte, byte bitOffset) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/BitSet/UInt10.cs b/src/BitSet/UInt10.cs new file mode 100644 index 0000000..c5c08e3 --- /dev/null +++ b/src/BitSet/UInt10.cs @@ -0,0 +1,52 @@ +using System; +using System.Runtime.CompilerServices; + +namespace BitSet +{ + public static partial class BitReader + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ushort ReadUInt10(byte[] buffer, int startByte = 0) + { + throw new NotImplementedException(); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ushort ReadUInt10(byte[] buffer, int startByte, byte bitOffset) + { + throw new NotImplementedException(); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe static ushort ReadUInt10(byte* buffer, int startByte = 0) + { + throw new NotImplementedException(); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe static ushort ReadUInt10(byte* buffer, int startByte, byte bitOffset) + { + throw new NotImplementedException(); + } + } + public static partial class BitWriter + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteUInt10(ushort value, byte[] buffer, int startByte = 0) + { + throw new NotImplementedException(); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteUInt10(ushort value, byte[] buffer, int startByte, byte bitOffset) + { + throw new NotImplementedException(); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe static void WriteUInt10(ushort value, byte* buffer, int startByte = 0) + { + throw new NotImplementedException(); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe static void WriteUInt10(ushort value, byte* buffer, int startByte, byte bitOffset) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/BitSet/UInt16.cs b/src/BitSet/UInt16.cs new file mode 100644 index 0000000..a2a8cc5 --- /dev/null +++ b/src/BitSet/UInt16.cs @@ -0,0 +1,99 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace BitSet +{ + public static partial class BitReader + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint ReadUInt16(byte[] buffer, ref ulong startBit) + { + var retVal = ReadUInt16(buffer, startBit); + startBit += 16; + return retVal; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint ReadUInt16(byte[] buffer, ulong startBit) + { + return ReadUInt16(buffer, (int)(startBit / 8), (byte)(startBit % 8)); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ushort ReadUInt16(byte[] buffer, int startByte = 0) + { + return (ushort)(buffer[startByte] | (buffer[startByte + 1] << 8)); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ushort ReadUInt16(byte[] buffer, int startByte, byte bitOffset) + { + switch (bitOffset) + { + case 0: return ReadUInt16(buffer, startByte); + + case 1: + case 2: + case 3: + case 4: + case 5: + case 6: + case 7: + return (ushort)((ReadUInt24(buffer, startByte) >> bitOffset) & 0xFFFF); + } + + throw new ArgumentOutOfRangeException(nameof(bitOffset)); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ushort ReadUInt16(IReadOnlyList buffer, int startByte = 0) + { + throw new NotImplementedException(); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ushort ReadUInt16(IReadOnlyList buffer, int startByte, byte bitOffset) + { + throw new NotImplementedException(); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe static ushort ReadUInt16(byte* buffer, int startByte = 0) + { + return *(ushort*)(&buffer[startByte]); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe static ushort ReadUInt16(byte* buffer, int startByte, byte bitOffset) + { + throw new NotImplementedException(); + } + } + public static partial class BitWriter + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteUInt16(ushort value, byte[] buffer, int startByte = 0) + { + throw new NotImplementedException(); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteUInt16(ushort value, byte[] buffer, int startByte, byte bitOffset) + { + throw new NotImplementedException(); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteUInt16(ushort value, IList buffer, int startByte = 0) + { + throw new NotImplementedException(); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteUInt16(ushort value, IList buffer, int startByte, byte bitOffset) + { + throw new NotImplementedException(); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe static void WriteUInt16(ushort value, byte* buffer, int startByte = 0) + { + *(ushort*)(&buffer[startByte]) = value; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe static void WriteUInt16(ushort value, byte* buffer, int startByte, byte bitOffset) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/BitSet/UInt2.cs b/src/BitSet/UInt2.cs new file mode 100644 index 0000000..d8b1266 --- /dev/null +++ b/src/BitSet/UInt2.cs @@ -0,0 +1,93 @@ +using System; +using System.Diagnostics; +using System.Runtime.CompilerServices; + +namespace BitSet +{ + public static partial class BitReader + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte ReadUInt2(byte[] buffer, ref ulong startBit) + { + var retVal = ReadUInt2(buffer, startBit); + startBit += 2; + return retVal; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte ReadUInt2(byte[] buffer, ulong startBit) + { + return ReadUInt2(buffer, (int)(startBit / 8), (byte)(startBit % 8)); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte ReadUInt2(byte[] buffer, int startByte = 0) + { + throw new NotImplementedException(); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte ReadUInt2(byte[] buffer, int startByte, byte bitOffset) + { + throw new NotImplementedException(); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe static byte ReadUInt2(byte* buffer, int startByte = 0) + { + return (byte)(buffer[startByte] & 0x03); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe static byte ReadUInt2(byte* buffer, int startByte, byte bitOffset) + { + switch (bitOffset) + { + case 0: return ReadUInt2(buffer, startByte); + case 1: + case 2: + case 3: + case 4: + case 5: return (byte)((buffer[startByte] >> bitOffset) & 0x03); + case 6: return (byte)(buffer[startByte] >> bitOffset); + + case 7: + throw new NotImplementedException(); + } + + throw new ArgumentOutOfRangeException(nameof(bitOffset)); + } + } + public static partial class BitWriter + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteUInt2(byte value, byte[] buffer, int startByte = 0) + { + throw new NotImplementedException(); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteUInt2(byte value, byte[] buffer, int startByte, byte bitOffset) + { + throw new NotImplementedException(); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe static void WriteUInt2(byte value, byte* buffer, int startByte = 0) + { + buffer[startByte] = (byte)((buffer[startByte] & ~0x03) | (value & 0x03)); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe static void WriteUInt2(byte value, byte* buffer, int startByte, byte bitOffset) + { + switch (bitOffset) + { + case 0: WriteUInt2(value, buffer, startByte); return; + case 1: buffer[startByte] = (byte)((buffer[startByte] & ~0x06) | ((value << 1) & 0x06)); return; + case 2: buffer[startByte] = (byte)((buffer[startByte] & ~0x0C) | ((value << 2) & 0x0C)); return; + case 3: buffer[startByte] = (byte)((buffer[startByte] & ~0x18) | ((value << 3) & 0x18)); return; + case 4: buffer[startByte] = (byte)((buffer[startByte] & ~0x30) | ((value << 4) & 0x30)); return; + case 5: buffer[startByte] = (byte)((buffer[startByte] & ~0x60) | ((value << 5) & 0x60)); return; + case 6: buffer[startByte] = (byte)((buffer[startByte] & ~0xC0) | (value << 6)); return; + + case 7: + throw new NotImplementedException(); + } + + throw new ArgumentOutOfRangeException(nameof(bitOffset)); + } + } +} diff --git a/src/BitSet/UInt20.cs b/src/BitSet/UInt20.cs new file mode 100644 index 0000000..e1ef27a --- /dev/null +++ b/src/BitSet/UInt20.cs @@ -0,0 +1,80 @@ +using System; +using System.Runtime.CompilerServices; + +namespace BitSet +{ + public static partial class BitReader + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint ReadUInt20(byte[] buffer, ref ulong startBit) + { + var retVal = ReadUInt20(buffer, startBit); + startBit += 20; + return retVal; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint ReadUInt20(byte[] buffer, ulong startBit) + { + return ReadUInt20(buffer, (int)(startBit / 8), (byte)(startBit % 8)); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint ReadUInt20(byte[] buffer, int startByte = 0) + { + throw new NotImplementedException(); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint ReadUInt20(byte[] buffer, int startByte, byte bitOffset) + { + switch (bitOffset) + { + case 0: return ReadUInt20(buffer, startByte); + + case 1: + case 2: + case 3: + case 4: + return (ReadUInt24(buffer, startByte) >> bitOffset) & 0xFFFFF; + + case 5: + case 6: + case 7: + return (ReadUInt32(buffer, startByte) >> bitOffset) & 0xFFFFF; + } + + throw new ArgumentOutOfRangeException(nameof(bitOffset)); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe static uint ReadUInt20(byte* buffer, int startByte = 0) + { + throw new NotImplementedException(); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe static uint ReadUInt20(byte* buffer, int startByte, byte bitOffset) + { + throw new NotImplementedException(); + } + } + public static partial class BitWriter + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteUInt20(uint value, byte[] buffer, int startByte = 0) + { + throw new NotImplementedException(); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteUInt20(uint value, byte[] buffer, int startByte, byte bitOffset) + { + throw new NotImplementedException(); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe static void WriteUInt20(uint value, byte* buffer, int startByte = 0) + { + throw new NotImplementedException(); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe static void WriteUInt20(uint value, byte* buffer, int startByte, byte bitOffset) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/BitSet/UInt24.cs b/src/BitSet/UInt24.cs new file mode 100644 index 0000000..3b416bd --- /dev/null +++ b/src/BitSet/UInt24.cs @@ -0,0 +1,64 @@ +using System; +using System.Runtime.CompilerServices; + +namespace BitSet +{ + public static partial class BitReader + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint ReadUInt24(byte[] buffer, ref ulong startBit) + { + var retVal = ReadUInt24(buffer, startBit); + startBit += 24; + return retVal; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint ReadUInt24(byte[] buffer, ulong startBit) + { + return ReadUInt24(buffer, (int)(startBit / 8), (byte)(startBit % 8)); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint ReadUInt24(byte[] buffer, int startByte = 0) + { + return (uint)(ReadUInt16(buffer, startByte) | (ReadUInt8(buffer, startByte + 2) << 16)); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint ReadUInt24(byte[] buffer, int startByte, byte bitOffset) + { + throw new NotImplementedException(); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe static uint ReadUInt24(byte* buffer, int startByte = 0) + { + throw new NotImplementedException(); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe static uint ReadUInt24(byte* buffer, int startByte, byte bitOffset) + { + throw new NotImplementedException(); + } + } + public static partial class BitWriter + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteUInt24(uint value, byte[] buffer, int startByte = 0) + { + throw new NotImplementedException(); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteUInt24(uint value, byte[] buffer, int startByte, byte bitOffset) + { + throw new NotImplementedException(); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe static void WriteUInt24(uint value, byte* buffer, int startByte = 0) + { + throw new NotImplementedException(); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe static void WriteUInt24(uint value, byte* buffer, int startByte, byte bitOffset) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/BitSet/UInt3.cs b/src/BitSet/UInt3.cs new file mode 100644 index 0000000..d1e8ee8 --- /dev/null +++ b/src/BitSet/UInt3.cs @@ -0,0 +1,64 @@ +using System; +using System.Runtime.CompilerServices; + +namespace BitSet +{ + public static partial class BitReader + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte ReadUInt3(byte[] buffer, ref ulong startBit) + { + var retVal = ReadUInt3(buffer, startBit); + startBit += 3; + return retVal; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte ReadUInt3(byte[] buffer, ulong startBit) + { + return ReadUInt3(buffer, (int)(startBit / 8), (byte)(startBit % 8)); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte ReadUInt3(byte[] buffer, int startByte = 0) + { + throw new NotImplementedException(); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte ReadUInt3(byte[] buffer, int startByte, byte bitOffset) + { + throw new NotImplementedException(); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe static byte ReadUInt3(byte* buffer, int startByte = 0) + { + throw new NotImplementedException(); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe static byte ReadUInt3(byte* buffer, int startByte, byte bitOffset) + { + throw new NotImplementedException(); + } + } + public static partial class BitWriter + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteUInt3(byte value, byte[] buffer, int startByte = 0) + { + throw new NotImplementedException(); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteUInt3(byte value, byte[] buffer, int startByte, byte bitOffset) + { + throw new NotImplementedException(); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe static void WriteUInt3(byte value, byte* buffer, int startByte = 0) + { + throw new NotImplementedException(); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe static void WriteUInt3(byte value, byte* buffer, int startByte, byte bitOffset) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/BitSet/UInt32.cs b/src/BitSet/UInt32.cs new file mode 100644 index 0000000..ef894f8 --- /dev/null +++ b/src/BitSet/UInt32.cs @@ -0,0 +1,78 @@ +using System; +using System.Runtime.CompilerServices; + +namespace BitSet +{ + public static partial class BitReader + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint ReadUInt32(byte[] buffer, ref ulong startBit) + { + var retVal = ReadUInt32(buffer, startBit); + startBit += 32; + return retVal; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint ReadUInt32(byte[] buffer, ulong startBit) + { + return ReadUInt32(buffer, (int)(startBit / 8), (byte)(startBit % 8)); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint ReadUInt32(byte[] buffer, int startByte = 0) + { + return BitConverter.ToUInt32(buffer, startByte); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint ReadUInt32(byte[] buffer, int startByte, byte bitOffset) + { + switch (bitOffset) + { + case 0: return ReadUInt32(buffer, startByte); + + case 1: + case 2: + case 3: + case 4: + case 5: + case 6: + case 7: + return (uint)((ReadUInt48(buffer, startByte) >> bitOffset) & 0xFFFFFFFF); + } + + throw new ArgumentOutOfRangeException(nameof(bitOffset)); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe static uint ReadUInt32(byte* buffer, int startByte = 0) + { + throw new NotImplementedException(); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe static uint ReadUInt32(byte* buffer, int startByte, byte bitOffset) + { + throw new NotImplementedException(); + } + } + public static partial class BitWriter + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteUInt32(uint value, byte[] buffer, int startByte = 0) + { + throw new NotImplementedException(); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteUInt32(uint value, byte[] buffer, int startByte, byte bitOffset) + { + throw new NotImplementedException(); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe static void WriteUInt32(uint value, byte* buffer, int startByte = 0) + { + throw new NotImplementedException(); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe static void WriteUInt32(uint value, byte* buffer, int startByte, byte bitOffset) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/BitSet/UInt4.cs b/src/BitSet/UInt4.cs new file mode 100644 index 0000000..847f756 --- /dev/null +++ b/src/BitSet/UInt4.cs @@ -0,0 +1,94 @@ +using System; +using System.Runtime.CompilerServices; + +namespace BitSet +{ + public static partial class BitReader + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte ReadUInt4(byte[] buffer, ref ulong startBit) + { + var retVal = ReadUInt4(buffer, startBit); + startBit += 4; + return retVal; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte ReadUInt4(byte[] buffer, ulong startBit) + { + return ReadUInt4(buffer, (int)(startBit / 8), (byte)(startBit % 8)); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte ReadUInt4(byte[] buffer, int startByte = 0) + { + return (byte)(buffer[startByte] & 0xF); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte ReadUInt4(byte[] buffer, int startByte, byte bitOffset) + { + throw new NotImplementedException(); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe static byte ReadUInt4(byte* buffer, int startByte = 0) + { + return (byte)(buffer[startByte] & 0xF); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe static byte ReadUInt4(byte* buffer, int startByte, byte bitOffset) + { + switch (bitOffset) + { + case 0: + case 1: + case 2: + case 3: + return (byte)((buffer[startByte] >> bitOffset) & 0xF); + + case 4: + case 5: + case 6: + case 7: + return (byte)((*(ushort*)(&buffer[startByte]) >> bitOffset) & 0xF); + } + + throw new ArgumentOutOfRangeException(nameof(bitOffset)); + } + } + public static partial class BitWriter + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteUInt4(byte value, byte[] buffer, int startByte = 0) + { + throw new NotImplementedException(); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteUInt4(byte value, byte[] buffer, int startByte, byte bitOffset) + { + throw new NotImplementedException(); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe static void WriteUInt4(byte value, byte* buffer, int startByte = 0) + { + buffer[startByte] = (byte)((buffer[startByte] & ~0x0F) | (value & 0x0F)); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe static void WriteUInt4(byte value, byte* buffer, int startByte, byte bitOffset) + { + switch (bitOffset) + { + case 0: WriteUInt4(value, buffer, startByte); return; + + case 4: buffer[startByte] = (byte)((buffer[startByte] & 0x0F) | (value << 4)); return; + + case 1: + case 2: + case 3: + case 5: + case 6: + case 7: + throw new NotImplementedException(); + } + + throw new ArgumentOutOfRangeException(nameof(bitOffset)); + } + } +} diff --git a/src/BitSet/UInt48.cs b/src/BitSet/UInt48.cs new file mode 100644 index 0000000..39402c8 --- /dev/null +++ b/src/BitSet/UInt48.cs @@ -0,0 +1,63 @@ +using System; +using System.Runtime.CompilerServices; + +namespace BitSet +{ public static partial class BitReader + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong ReadUInt48(byte[] buffer, ref ulong startBit) + { + var retVal = ReadUInt48(buffer, startBit); + startBit += 48; + return retVal; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong ReadUInt48(byte[] buffer, ulong startBit) + { + return ReadUInt48(buffer, (int)(startBit / 8), (byte)(startBit % 8)); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong ReadUInt48(byte[] buffer, int startByte = 0) + { + return ReadUInt32(buffer, startByte) | ((ulong)ReadUInt8(buffer, startByte + 4) << 32); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong ReadUInt48(byte[] buffer, int startByte, byte bitOffset) + { + throw new NotImplementedException(); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe static ulong ReadUInt48(byte* buffer, int startByte = 0) + { + throw new NotImplementedException(); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe static ulong ReadUInt48(byte* buffer, int startByte, byte bitOffset) + { + throw new NotImplementedException(); + } + } + public static partial class BitWriter + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteUInt48(ulong value, byte[] buffer, int startByte = 0) + { + throw new NotImplementedException(); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteUInt48(ulong value, byte[] buffer, int startByte, byte bitOffset) + { + throw new NotImplementedException(); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe static void WriteUInt48(ulong value, byte* buffer, int startByte = 0) + { + throw new NotImplementedException(); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe static void WriteUInt48(ulong value, byte* buffer, int startByte, byte bitOffset) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/BitSet/UInt5.cs b/src/BitSet/UInt5.cs new file mode 100644 index 0000000..3f41173 --- /dev/null +++ b/src/BitSet/UInt5.cs @@ -0,0 +1,123 @@ +using System; +using System.Diagnostics; +using System.Runtime.CompilerServices; + +namespace BitSet +{ + public static partial class BitReader + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte ReadUInt5(byte[] buffer, ref ulong startBit) + { + var retVal = ReadUInt5(buffer, startBit); + startBit += 5; + return retVal; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte ReadUInt5(byte[] buffer, ulong startBit) + { + return ReadUInt5(buffer, (int)(startBit / 8), (byte)(startBit % 8)); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte ReadUInt5(byte[] buffer, int startByte = 0) + { + throw new NotImplementedException(); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte ReadUInt5(byte[] buffer, int startByte, byte bitOffset) + { + switch (bitOffset) + { + case 0: return ReadUInt5(buffer, startByte); + + case 1: + case 2: + case 3: + return (byte)((buffer[startByte] >> bitOffset) & 0x1F); + + case 4: + case 5: + case 6: + case 7: + return (byte)((ReadUInt16(buffer, startByte) >> bitOffset) & 0x1F); + } + + throw new ArgumentOutOfRangeException(nameof(bitOffset)); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe static byte ReadUInt5(byte* buffer, int startByte = 0) + { + return (byte)(buffer[startByte] & 0x1F); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe static byte ReadUInt5(byte* buffer, int startByte, byte bitOffset) + { + switch (bitOffset) + { + case 0: return ReadUInt5(buffer, startByte); + + case 2: return (byte)((buffer[startByte] & 0x7C) >> 2); + case 3: return (byte)((buffer[startByte] & 0xF8) >> 3); + + case 5: return (byte)(((buffer[startByte + 1] & 0x03) << 3) | ((buffer[startByte] & 0xE0) >> 5)); + + case 1: + + case 4: + + case 6: + case 7: + throw new NotImplementedException(); + } + + throw new ArgumentOutOfRangeException(nameof(bitOffset)); + } + } + public static partial class BitWriter + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteUInt5(byte value, byte[] buffer, int startByte = 0) + { + throw new NotImplementedException(); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteUInt5(byte value, byte[] buffer, int startByte, byte bitOffset) + { + throw new NotImplementedException(); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe static void WriteUInt5(byte value, byte* buffer, int startByte = 0) + { + buffer[startByte] = (byte)((buffer[startByte] & ~0x1F) | (value & 0x1F)); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe static void WriteUInt5(byte value, byte* buffer, int startByte, byte bitOffset) + { + switch (bitOffset) + { + case 0: WriteUInt5(value, buffer, startByte); return; + + case 2: buffer[startByte] = (byte)((buffer[startByte] & ~0x7C) | ((value << 2) & 0x7C)); return; + case 3: buffer[startByte] = (byte)((buffer[startByte] & ~0xF8) | ((value << 3) & 0xF8)); return; + + case 5: + { + buffer[startByte + 1] = (byte)((buffer[startByte + 1] & ~0x03) | ((value >> 3) & 0x03)); + buffer[startByte] = (byte)((buffer[startByte] & ~0xE0) | ((value << 5) & 0xE0)); + Debug.Assert(((*(ushort*)(&buffer[startByte]) & 0x3E0) >> 5) == (value & 0x1F)); + return; + } + + case 1: + + case 4: + + case 6: + case 7: + throw new NotImplementedException(); + } + + throw new ArgumentOutOfRangeException(nameof(bitOffset)); + } + } +} diff --git a/src/BitSet/UInt6.cs b/src/BitSet/UInt6.cs new file mode 100644 index 0000000..3fdb51f --- /dev/null +++ b/src/BitSet/UInt6.cs @@ -0,0 +1,113 @@ +using System; +using System.Diagnostics; +using System.Runtime.CompilerServices; + +namespace BitSet +{ + public static partial class BitReader + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte ReadUInt6(byte[] buffer, ref ulong startBit) + { + var retVal = ReadUInt6(buffer, startBit); + startBit += 6; + return retVal; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte ReadUInt6(byte[] buffer, ulong startBit) + { + return ReadUInt6(buffer, (int)(startBit / 8), (byte)(startBit % 8)); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte ReadUInt6(byte[] buffer, int startByte = 0) + { + return (byte)(buffer[startByte] & 0x3F); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte ReadUInt6(byte[] buffer, int startByte, byte bitOffset) + { + switch (bitOffset) + { + case 0: return (byte)(buffer[startByte] & 0x3F); + case 1: return (byte)((buffer[startByte] & 0x7E) >> 1); + case 2: return (byte)((buffer[startByte] & 0xFC) >> 2); + + case 3: + case 4: + case 5: + case 6: + case 7: + return (byte)((ReadUInt16(buffer, startByte) >> bitOffset) & 0x3F); + } + + throw new ArgumentOutOfRangeException(nameof(bitOffset)); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe static byte ReadUInt6(byte* buffer, int startByte = 0) + { + throw new NotImplementedException(); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe static byte ReadUInt6(byte* buffer, int startByte, byte bitOffset) + { + switch (bitOffset) + { + case 5: return (byte)(((buffer[startByte + 1] & 0x07) << 3) | ((buffer[startByte] & 0xE0) >> 5)); + + case 0: + case 1: + case 2: + case 3: + case 4: + case 6: + case 7: + throw new NotImplementedException(); + } + + throw new ArgumentOutOfRangeException(nameof(bitOffset)); + } + } + public static partial class BitWriter + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteUInt6(byte value, byte[] buffer, int startByte = 0) + { + throw new NotImplementedException(); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteUInt6(byte value, byte[] buffer, int startByte, byte bitOffset) + { + throw new NotImplementedException(); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe static void WriteUInt6(byte value, byte* buffer, int startByte = 0) + { + throw new NotImplementedException(); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe static void WriteUInt6(byte value, byte* buffer, int startByte, byte bitOffset) + { + switch (bitOffset) + { + case 5: + { + buffer[startByte + 1] = (byte)((buffer[startByte + 1] & ~0x07) | ((value >> 3) & 0x07)); + buffer[startByte] = (byte)((buffer[startByte] & ~0xE0) | ((value << 5) & 0xE0 )); + Debug.Assert(((*(ushort*)(&buffer[startByte]) & 0x7E0) >> 5) == (value & 0x3F)); + return; + } + + case 0: + case 1: + case 2: + case 3: + case 4: + case 6: + case 7: + throw new NotImplementedException(); + } + + throw new ArgumentOutOfRangeException(nameof(bitOffset)); + } + } +} diff --git a/src/BitSet/UInt8.cs b/src/BitSet/UInt8.cs new file mode 100644 index 0000000..b125316 --- /dev/null +++ b/src/BitSet/UInt8.cs @@ -0,0 +1,92 @@ +using System; +using System.Runtime.CompilerServices; + +namespace BitSet +{ + public static partial class BitReader + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte ReadUInt8(byte[] buffer, ref ulong startBit) + { + var retVal = ReadUInt8(buffer, startBit); + startBit += 8; + return retVal; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte ReadUInt8(byte[] buffer, ulong startBit) + { + return ReadUInt8(buffer, (int)(startBit / 8), (byte)(startBit % 8)); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte ReadUInt8(byte[] buffer, int startByte = 0) + { + return buffer[startByte]; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte ReadUInt8(byte[] buffer, int startByte, byte bitOffset) + { + throw new NotImplementedException(); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe static byte ReadUInt8(byte* buffer, int startByte = 0) + { + return buffer[startByte]; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe static byte ReadUInt8(byte* buffer, int startByte, byte bitOffset) + { + switch (bitOffset) + { + case 0: return ReadUInt8(buffer, startByte); + + case 1: + case 2: + case 3: + case 4: + case 5: + case 6: + case 7: + throw new NotImplementedException(); + } + + throw new ArgumentOutOfRangeException(nameof(bitOffset)); + } + } + public static partial class BitWriter + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteUInt8(byte value, byte[] buffer, int startByte = 0) + { + buffer[startByte] = value; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteUInt8(byte value, byte[] buffer, int startByte, byte bitOffset) + { + throw new NotImplementedException(); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe static void WriteUInt8(byte value, byte* buffer, int startByte = 0) + { + buffer[startByte] = value; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public unsafe static void WriteUInt8(byte value, byte* buffer, int startByte, byte bitOffset) + { + switch (bitOffset) + { + case 0: WriteUInt8(value, buffer, startByte); return; + + case 1: + case 2: + case 3: + case 4: + case 5: + case 6: + case 7: + throw new NotImplementedException(); + } + + throw new ArgumentOutOfRangeException(nameof(bitOffset)); + } + } +} diff --git a/src/BitSet/VarInt.cs b/src/BitSet/VarInt.cs new file mode 100644 index 0000000..0546fbb --- /dev/null +++ b/src/BitSet/VarInt.cs @@ -0,0 +1,23 @@ +namespace BitSet +{ + public static partial class BitReader + { + const int MAX_VARINT_BITS = 35; + + public static uint ReadVarInt(byte[] buffer, ref ulong bitOffset) + { + uint dest = 0; + + for (byte run = 0; run < 35; run += 7) + { + byte oneByte = (byte)ReadUIntBits(buffer, ref bitOffset, 8); + dest |= ((oneByte & (uint)0x7F) << run); + + if ((oneByte >> 7) == 0) + break; + } + + return dest; + } + } +} diff --git a/src/DemoCommands.cs b/src/DemoCommands.cs new file mode 100644 index 0000000..fe6b00c --- /dev/null +++ b/src/DemoCommands.cs @@ -0,0 +1,299 @@ +using System.Diagnostics; +using System.Text; +using BitSet; +using DemoParser; +using TF2Net; +using TF2Net.Data; +using TF2Net.NetMessages; + +namespace DemoLib.Commands +{ + public enum DemoCommandType : byte { + + dem_invalid = 0, + + // it's a startup message, process as fast as possible + dem_signon = 1, + // it's a normal network packet that we stored off + dem_packet, + // sync client clock to demo tick + dem_synctick, + // console command + dem_consolecmd, + // user input command + dem_usercmd, + // network data tables + dem_datatables, + // end of time. + dem_stop, + + dem_stringtables, + + // Last command -- not necessary in C# + //dem_lastcmd = dem_stringtables + } + + public class DemoCommand + { + public DemoCommandType Type { get; protected set; } = DemoCommandType.dem_invalid; + } + + class TimestampedDemoCommand : DemoCommand + { + public int Tick { get; set; } + + public TimestampedDemoCommand(Stream input) + { + using (BinaryReader reader = new BinaryReader(input, Encoding.Default, true)) + { + Tick = reader.ReadInt32(); + } + } + } + + sealed class DemoConsoleCommand : TimestampedDemoCommand + { + public string Command { get; set; } + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private string DebuggerDisplayAttributeValue + { + get { return Command.Replace('"', '\''); } + } + + public DemoConsoleCommand(Stream input) : base(input) + { + Type = DemoCommandType.dem_consolecmd; + + using (BinaryReader reader = new BinaryReader(input, Encoding.ASCII, true)) + Command = new string(reader.ReadChars(reader.ReadInt32())).TrimEnd('\0'); + } + } + + class DemoDataTablesCommand : TimestampedDemoCommand { + const int PROPINFOBITS_NUMPROPS = 10; + const int PROPINFOBITS_TYPE = 5; + const int PROPINFOBITS_FLAGS = SPROP_NUMFLAGBITS_NETWORKED; + const int PROPINFOBITS_NUMELEMENTS = 10; + const int PROPINFOBITS_NUMBITS = 7; + + const int SPROP_NUMFLAGBITS_NETWORKED = 16; + + public IList SendTables { get; set; } = new List(); + public IList ServerClasses { get; set; } = new List(); + + public DemoDataTablesCommand(Stream input) : base(input) + { + Type = DemoCommandType.dem_datatables; + + BitStream stream; + using (BinaryReader reader = new BinaryReader(input, Encoding.ASCII, true)) + { + int length = reader.ReadInt32(); + stream = new BitStream(reader.ReadBytes(length)); + } + + while (stream.ReadBool()) + SendTables.Add(ParseSendTable(stream)); + + // Link referenced datatables + foreach (SendTable table in SendTables) + { + foreach (SendPropDefinition dtProp in table.Properties) + { + if (dtProp.Type == SendPropType.Datatable) + { + dtProp.Table = SendTables.Single(t => t.NetTableName == dtProp.ExcludeName); + dtProp.ExcludeName = null; + } + } + } + + short serverClasses = stream.ReadShort(); + Debug.Assert(serverClasses > 0); + + ServerClasses = new List(serverClasses); + + for (int i = 0; i < serverClasses; i++) + { + short classID = stream.ReadShort(); + if (classID >= serverClasses) + throw new Exception("Demo parsing failed: Invalid server class ID"); + + ServerClass sc = new ServerClass(); + sc.Classname = stream.ReadCString(); + sc.DatatableName = stream.ReadCString(); + ServerClasses.Add(sc); + } + + Debug.Assert((stream.Length - stream.Cursor) < 8); + } + + static SendTable ParseSendTable(BitStream stream) + { + SendTable table = new SendTable(); + + table.Unknown1 = stream.ReadBool(); + + table.NetTableName = stream.ReadCString(); + + int propertyCount = (int)stream.ReadULong(PROPINFOBITS_NUMPROPS); + + SendPropDefinition arrayElementProp = null; + + for (int i = 0; i < propertyCount; i++) + { + SendPropDefinition prop = new SendPropDefinition(table); + + prop.Type = (SendPropType)stream.ReadULong(PROPINFOBITS_TYPE); + Debug.Assert(Enum.GetValues(typeof(SendPropType)).Cast().Contains(prop.Type)); + + Debug.Assert(prop.Type == SendPropType.Datatable ? prop.Flags == 0 : true); + + prop.Name = stream.ReadCString(); + + prop.Flags = (SendPropFlags)stream.ReadULong(PROPINFOBITS_FLAGS); + + if (prop.Type == SendPropType.Datatable) + { + prop.ExcludeName = stream.ReadCString(); + } + else + { + if ((prop.Flags & SendPropFlags.Exclude) != 0) + prop.ExcludeName = stream.ReadCString(); + else if (prop.Type == SendPropType.Array) + prop.ArrayElements = (int)stream.ReadULong(PROPINFOBITS_NUMELEMENTS); + else + { + prop.LowValue = stream.ReadSingle(); + prop.HighValue = stream.ReadSingle(); + + prop.BitCount = stream.ReadULong(PROPINFOBITS_NUMBITS); + } + } + + if (prop.Flags.HasFlag(SendPropFlags.NoScale)) + { + if (prop.Type == SendPropType.Float) + prop.BitCount = 32; + else if (prop.Type == SendPropType.Vector) + { + if (!prop.Flags.HasFlag(SendPropFlags.Normal)) + prop.BitCount = 32 * 3; + } + } + + if (arrayElementProp != null) + { + Debug.Assert(prop.Type == SendPropType.Array); + prop.ArrayProperty = arrayElementProp; + arrayElementProp = null; + } + + if (prop.Flags.HasFlag(SendPropFlags.InsideArray)) + { + Debug.Assert(arrayElementProp == null); + Debug.Assert(!prop.Flags.HasFlag(SendPropFlags.ChangesOften)); + arrayElementProp = prop; + } + else + { + table.Properties.Add(prop); + } + } + + return table; + } + } + + sealed class DemoUserCommand : TimestampedDemoCommand + { + public int OutgoingSequence { get; set; } + + public byte[] Data { get; set; } + + public DemoUserCommand(Stream input) : base(input) + { + Type = DemoCommandType.dem_usercmd; + + using (BinaryReader reader = new BinaryReader(input, Encoding.ASCII, true)) + { + OutgoingSequence = reader.ReadInt32(); + + Data = reader.ReadBytes(reader.ReadInt32()); + } + } + } + + sealed class DemoSyncTickCommand : TimestampedDemoCommand + { + public DemoSyncTickCommand(Stream input) : base(input) + { + Type = DemoCommandType.dem_synctick; + } + } + + sealed class DemoStringTablesCommand : TimestampedDemoCommand + { + public BitStream Data { get; set; } + + public DemoStringTablesCommand(Stream input) : base(input) + { + Type = DemoCommandType.dem_stringtables; + + using (BinaryReader reader = new BinaryReader(input, Encoding.ASCII, true)) + { + int dataLength = reader.ReadInt32(); + Data = new BitStream(reader.ReadBytes(dataLength)); + } + } + } + + sealed class DemoSignonCommand : DemoPacketCommand + { + public DemoSignonCommand(Stream input, ulong signonLength) : base(input) + { + Type = DemoCommandType.dem_signon; + } + } + + class DemoPacketCommand : TimestampedDemoCommand + { + public DemoViewpoint Viewpoint { get; set; } + + public int SequenceIn { get; set; } + public int SequenceOut { get; set; } + + public IList Messages { get; set; } + + public DemoPacketCommand(Stream input) : base(input) + { + Type = DemoCommandType.dem_packet; + + using (BinaryReader r = new BinaryReader(input, Encoding.ASCII, true)) + { + Viewpoint = new DemoViewpoint(); + + Viewpoint.ViewpointFlags = (DemoViewpoint.Flags)r.ReadInt32(); + + Viewpoint.ViewOrigin1 = new Vector(r.ReadSingle(), r.ReadSingle(), r.ReadSingle()); + Viewpoint.ViewAngles1 = new Vector(r.ReadSingle(), r.ReadSingle(), r.ReadSingle()); + Viewpoint.LocalViewAngles1 = new Vector(r.ReadSingle(), r.ReadSingle(), r.ReadSingle()); + + Viewpoint.ViewOrigin2 = new Vector(r.ReadSingle(), r.ReadSingle(), r.ReadSingle()); + Viewpoint.ViewAngles2 = new Vector(r.ReadSingle(), r.ReadSingle(), r.ReadSingle()); + Viewpoint.LocalViewAngles2 = new Vector(r.ReadSingle(), r.ReadSingle(), r.ReadSingle()); + + SequenceIn = r.ReadInt32(); + SequenceOut = r.ReadInt32(); + + BitStream data = new BitStream(r.ReadBytes((int)r.ReadUInt32())); + Messages = NetMessageCoder.Decode(data).ToArray(); + } + } + } + + + +} \ No newline at end of file diff --git a/src/Program.cs b/src/Program.cs index 5d3e1a1..02d385d 100644 --- a/src/Program.cs +++ b/src/Program.cs @@ -1,6 +1,9 @@ // See https://aka.ms/new-console-template for more information using System.Text; +using DemoLib.Commands; using DemoParser; +using TF2Net; +using TF2Net.Data; Console.WriteLine("Hello, World!"); //TODO: Config @@ -11,8 +14,8 @@ var demoFilePaths = Directory.GetFiles(_DemoPath, "*.dem"); foreach(var currentDemoPath in demoFilePaths){ var demo = File.OpenRead(Path.Combine(_DemoPath, currentDemoPath)); - var info = DemoParsing.ReadDemoHeader(demo); - Console.WriteLine($"{currentDemoPath}: Header parsed "); + var demoInfo = new DemoReader(demo); + Console.WriteLine("Successfully read demo!"); } @@ -22,35 +25,77 @@ foreach(var currentDemoPath in demoFilePaths){ namespace DemoParser{ - public record demoHeader( - int demoProtocol, - int networkProtocol, - string ServerName, - string ClientName, - string MapName, - string GameDirectory, - float playbackTime, - int Ticks, - int Frames, - int SignOnLength - ); - - public class DemoParsing{ - public static demoHeader ReadDemoHeader(FileStream stream){ - // 8 chars - // int - // int - // 260 chars - // 260 chars - // 260 chars - // 260 chars - //float - //int - //int - //int - - //Read the header. + class DemoViewpoint { + [Flags] + public enum Flags + { + None = 0, + UseOrigin2 = (1 << 0), + UseAngles2 = (1 << 1), + NoInterp = (1 << 2), + } + + public Flags ViewpointFlags { get; set; } + + public Vector ViewOrigin1 { get; set; } + public Vector ViewAngles1 { get; set; } + public Vector LocalViewAngles1 { get; set; } + + public Vector ViewOrigin2 { get; set; } + public Vector ViewAngles2 { get; set; } + public Vector LocalViewAngles2 { get; set; } + } + + public class DemoHeader { + public int demoProtocol {get; set;} + public int networkProtocol {get; set;} + public string ServerName {get; set;} + public string ClientName {get; set;} + public string MapName {get; set;} + public string GameDirectory {get; set;} + public float playbackTime {get; set;} + public int Ticks {get; set;} + public int Frames {get; set;} + public int SignOnLength {get; set;} + + public DemoHeader(){} + public DemoHeader(Stream stream){ + // Note; order is important. + int demoProtocol = DemoReader.readInt(stream); + int networkProtocol = DemoReader.readInt(stream); + string ServerName = DemoReader.readString(stream, 260, Encoding.ASCII); + string ClientNam = DemoReader.readString(stream, 260, Encoding.ASCII); + string MapName = DemoReader.readString(stream, 260, Encoding.ASCII); + string GameDirectory= DemoReader.readString(stream, 260, Encoding.ASCII); + float playbackTime = DemoReader.readFloat(stream); + int Ticks = DemoReader.readInt(stream); + int Frames = DemoReader.readInt(stream); + int SignOnLength = DemoReader.readInt(stream); + } + }; + + public class DemoReader { + + public DemoHeader Header { get; private set; } + + public IReadOnlyList Commands { get; private set; } + readonly WorldEvents m_Events = new WorldEvents(); + public IWorldEvents Events { get { return m_Events; } } + + public DemoReader(Stream input) + { + Header = new DemoHeader(input); + + List commands = new List(); + Commands = commands; + + DemoCommand cmd = null; + while ((cmd = ParseCommand(input)) != null) + commands.Add(cmd); + } + + public static DemoHeader ReadDemoHeader(Stream stream){ // If the first 8 chars are not "HL2DEMO" then it is not valid. var buffer = new Byte[8]; @@ -60,36 +105,45 @@ namespace DemoParser{ throw new Exception($"DemoFile not valid! Filestamp: {Encoding.ASCII.GetString(buffer)}"); } - return new demoHeader( - readInt(stream), - readInt(stream), - readString(stream, 260, Encoding.ASCII), - readString(stream, 260, Encoding.ASCII), - readString(stream, 260, Encoding.ASCII), - readString(stream, 260, Encoding.ASCII), - readFloat(stream), - readInt(stream), - readInt(stream), - readInt(stream) - ); + return new DemoHeader(stream); } - public static int readInt(FileStream stream, int offset = 0){ + + DemoCommand ParseCommand(Stream input){ + DemoCommandType cmdType = (DemoCommandType)input.ReadByte(); + + switch (cmdType) + { + case DemoCommandType.dem_signon: return new DemoSignonCommand(input, (ulong)Header.SignOnLength); + case DemoCommandType.dem_packet: return new DemoPacketCommand(input); + case DemoCommandType.dem_synctick: return new DemoSyncTickCommand(input); + case DemoCommandType.dem_consolecmd: return new DemoConsoleCommand(input); + case DemoCommandType.dem_usercmd: return new DemoUserCommand(input); + case DemoCommandType.dem_datatables: return new DemoDataTablesCommand(input); + case DemoCommandType.dem_stop: return null; + case DemoCommandType.dem_stringtables: return new DemoStringTablesCommand(input); + + default: + throw new NotImplementedException(string.Format("Unknown command type {0}", cmdType)); + } + } + + public static int readInt(Stream stream, int offset = 0){ var buffer = new Byte[4]; stream.Read(buffer, offset, 4); return BitConverter.ToInt32(buffer,0); } - public static float readFloat(FileStream stream, int offset = 0){ + public static float readFloat(Stream stream, int offset = 0){ var buffer = new Byte[4]; stream.Read(buffer, offset, 4); return BitConverter.ToSingle(buffer,0); } //Reads until end of string - public static string readString(FileStream stream, int maxLength, Encoding encoding,int offset = 0){ + public static string readString(Stream stream, int maxLength, Encoding encoding,int offset = 0){ var buffer = new Byte[maxLength]; var readBytes = stream.Read(buffer, offset, maxLength); @@ -101,12 +155,4 @@ namespace DemoParser{ } - - // static class helper{ - - // public static string ReadCString(this BinaryReader reader, int length, Encoding encoding) - // { - // return encoding.GetString(reader.ReadBytes(length)).Split(new char[] { '\0' }, 2)[0]; - // } - // } } \ No newline at end of file diff --git a/src/TF2Net/ConditionalHashSet.cs b/src/TF2Net/ConditionalHashSet.cs new file mode 100644 index 0000000..983444b --- /dev/null +++ b/src/TF2Net/ConditionalHashSet.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; + +namespace TF2Net +{ + sealed class ConditionalHashSet where T : class + { + private readonly object locker = new object(); + private readonly List weakList = new List(); + private readonly ConditionalWeakTable weakDictionary = + new ConditionalWeakTable(); + + public void Add(T item) + { + lock (locker) + { + var reference = new WeakReference(item); + weakDictionary.Add(item, reference); + weakList.Add(reference); + Shrink(); + } + } + + public void Remove(T item) + { + lock (locker) + { + WeakReference reference; + + if (weakDictionary.TryGetValue(item, out reference)) + { + reference.Target = null; + weakDictionary.Remove(item); + } + } + } + + public T[] ToArray() + { + lock (locker) + { + return ( + from weakReference in weakList + let item = (T)weakReference.Target + where item != null + select item) + .ToArray(); + } + } + + private void Shrink() + { + // This method prevents the List from growing indefinitely, but + // might also cause a performance problem in some cases. + if (weakList.Capacity == weakList.Count) + { + weakList.RemoveAll(weak => !weak.IsAlive); + } + } + } +} diff --git a/src/TF2Net/Data/BaselineIndex.cs b/src/TF2Net/Data/BaselineIndex.cs new file mode 100644 index 0000000..d04ccab --- /dev/null +++ b/src/TF2Net/Data/BaselineIndex.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TF2Net.Data +{ + public enum BaselineIndex + { + Baseline0 = 0, + Baseline1 = 1, + } +} diff --git a/src/TF2Net/Data/Class.cs b/src/TF2Net/Data/Class.cs new file mode 100644 index 0000000..694bd84 --- /dev/null +++ b/src/TF2Net/Data/Class.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TF2Net.Data +{ + public enum Class + { + Undefined = 0, + + Scout = 1, + Sniper = 2, + Soldier = 3, + Demo = 4, + Medic = 5, + Heavy = 6, + Pyro = 7, + Spy = 8, + Engie = 9, + + Civilian = 10, + } +} diff --git a/src/TF2Net/Data/ClientFrame.cs b/src/TF2Net/Data/ClientFrame.cs new file mode 100644 index 0000000..9d56a1a --- /dev/null +++ b/src/TF2Net/Data/ClientFrame.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TF2Net.Data +{ + [DebuggerDisplay("ClientFrame: tick {ServerTick,nq}")] + public class ClientFrame + { + public ClientFrame(ulong tick) + { + ServerTick = tick; + } + + public int LastEntityIndex { get; set; } + public ulong ServerTick { get; } + + public BitArray TransmitEntity { get; } = new BitArray(SourceConstants.MAX_EDICTS); + public BitArray FromBaseline { get; } = new BitArray(SourceConstants.MAX_EDICTS); + public BitArray TransmitAlways { get; } = new BitArray(SourceConstants.MAX_EDICTS); + } +} diff --git a/src/TF2Net/Data/ConnectionState.cs b/src/TF2Net/Data/ConnectionState.cs new file mode 100644 index 0000000..91c626a --- /dev/null +++ b/src/TF2Net/Data/ConnectionState.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TF2Net.Data +{ + public enum ConnectionState : byte + { + /// + /// no state yet, about to connect + /// + None = 0, + + /// + /// client challenging server, all OOB packets + /// + Challenge = 1, + + /// + /// client is connected to server, netchans ready + /// + Connected = 2, + + /// + /// just got serverinfo and string tables + /// + New = 3, + + /// + /// received signon buffers + /// + Prespawn = 4, + + /// + /// ready to receive entity packets + /// + Spawn = 5, + + /// + /// we are fully connected, first non-delta packet received + /// + Full = 6, + + /// + /// server is changing level, please wait + /// + Changelevel = 7, + } +} diff --git a/src/TF2Net/Data/EHandle.cs b/src/TF2Net/Data/EHandle.cs new file mode 100644 index 0000000..6656c5d --- /dev/null +++ b/src/TF2Net/Data/EHandle.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using TF2Net.Entities; + +namespace TF2Net.Data +{ + [DebuggerDisplay("{Entity,nq}")] + public class EHandle + { + public WorldState World { get; } + public uint EntityIndex { get; } + public uint SerialNumber { get; } + + public Entity Entity + { + get + { + Entity potential = World.Entities[EntityIndex]; + if (potential?.SerialNumber == SerialNumber) + return potential; + + return null; + } + } + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + Entity DebugEntity { get { return Entity; } } + + public EHandle(WorldState ws, uint handle) + { + World = ws; + + EntityIndex = handle & ((1 << SourceConstants.MAX_EDICT_BITS) - 1); + SerialNumber = handle >> SourceConstants.MAX_EDICT_BITS; + } + + public override string ToString() + { + return Entity?.ToString() ?? string.Format("null {0}", nameof(EHandle)); + } + } +} diff --git a/src/TF2Net/Data/EntityData.cs b/src/TF2Net/Data/EntityData.cs new file mode 100644 index 0000000..48b9439 --- /dev/null +++ b/src/TF2Net/Data/EntityData.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TF2Net.Data +{ + public class EntityData + { + public uint Tick { get; } + + public EntityData(uint tick) + { + Tick = tick; + } + } +} diff --git a/src/TF2Net/Data/EntityUpdateType.cs b/src/TF2Net/Data/EntityUpdateType.cs new file mode 100644 index 0000000..2419611 --- /dev/null +++ b/src/TF2Net/Data/EntityUpdateType.cs @@ -0,0 +1,15 @@ +namespace DemoLib.DataExtraction +{ + public enum EntityUpdateType + { + EnterPVS = 0, // Entity came back into pvs, create new entity if one doesn't exist + + LeavePVS, // Entity left pvs + + DeltaEnt, // There is a delta for this entity. + PreserveEnt, // Entity stays alive but no delta ( could be LOD, or just unchanged ) + + Finished, // finished parsing entities successfully + Failed, // parsing error occured while reading entities + }; +} diff --git a/src/TF2Net/Data/FlattenedProp.cs b/src/TF2Net/Data/FlattenedProp.cs new file mode 100644 index 0000000..4f25e64 --- /dev/null +++ b/src/TF2Net/Data/FlattenedProp.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TF2Net.Data +{ + [DebuggerDisplay("{ToString(),nq}")] + public class FlattenedProp + { + public string FullName { get; set; } + + public SendPropDefinition Property { get; set; } + + public override string ToString() + { + string bitCount = (Property.BitCount.HasValue && Property.BitCount.Value > 0) ? string.Format("[{0}]", Property.BitCount.Value) : string.Empty; + + return string.Format("{0}{1} \"{2}\" ({3})", Property.Type, bitCount, FullName, Property.Flags); + } + } +} diff --git a/src/TF2Net/Data/GameEvent.cs b/src/TF2Net/Data/GameEvent.cs new file mode 100644 index 0000000..4056557 --- /dev/null +++ b/src/TF2Net/Data/GameEvent.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TF2Net.Data +{ + [DebuggerDisplay("Game event: {Declaration.Name}")] + public class GameEvent + { + public IReadOnlyGameEventDeclaration Declaration { get; set; } + public IDictionary Values { get; set; } + } +} diff --git a/src/TF2Net/Data/GameEventDeclaration.cs b/src/TF2Net/Data/GameEventDeclaration.cs new file mode 100644 index 0000000..61486a4 --- /dev/null +++ b/src/TF2Net/Data/GameEventDeclaration.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using TF2Net.Extensions; +using TF2Net; + +namespace TF2Net.Data +{ + public enum GameEventDataType + { + Local = 0, // not networked + String, // zero terminated ASCII string + Float, // float 32 bit + Long, // signed int 32 bit + Short, // signed int 16 bit + Byte, // unsigned int 8 bit + Bool, // unsigned int 1 bit + }; + + [DebuggerDisplay("Game event declaration: {Name}")] + public class GameEventDeclaration : IReadOnlyGameEventDeclaration + { + public int ID { get; set; } + public string Name { get; set; } + + public IDictionary Values { get; set; } + IReadOnlyDictionary IReadOnlyGameEventDeclaration.Values { get { return (IReadOnlyDictionary)Values; } } + } + + public interface IReadOnlyGameEventDeclaration + { + int ID { get; } + string Name { get; } + + IReadOnlyDictionary Values { get; } + } +} diff --git a/src/TF2Net/Data/IReadOnlyVector.cs b/src/TF2Net/Data/IReadOnlyVector.cs new file mode 100644 index 0000000..ef619d0 --- /dev/null +++ b/src/TF2Net/Data/IReadOnlyVector.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TF2Net.Data +{ + public interface IReadOnlyVector : ICloneable + { + double X { get; } + double Y { get; } + double Z { get; } + } + + [EditorBrowsable(EditorBrowsableState.Never)] + public static class IReadOnlyVectorExtensions + { + public static Vector Clone(this IReadOnlyVector v) + { + return new Vector(v.X, v.Y, v.Z); + } + } +} diff --git a/src/TF2Net/Data/LifeState.cs b/src/TF2Net/Data/LifeState.cs new file mode 100644 index 0000000..36384c1 --- /dev/null +++ b/src/TF2Net/Data/LifeState.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TF2Net.Data +{ + public enum LifeState + { + Alive = 0, + + /// + /// playing death animation or still falling off of a ledge waiting to hit ground + /// + Dying = 1, + + /// + /// dead. lying still. + /// + Dead = 2, + Respawnable = 3, + DiscardBody = 4, + } +} diff --git a/src/TF2Net/Data/Player.cs b/src/TF2Net/Data/Player.cs new file mode 100644 index 0000000..352ca86 --- /dev/null +++ b/src/TF2Net/Data/Player.cs @@ -0,0 +1,415 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using BitSet; +using TF2Net.Entities; +using TF2Net.Monitors; + +namespace TF2Net.Data +{ + [DebuggerDisplay("{ToString(),nq}")] + public class Player + { + public WorldState World { get; } + + public uint EntityIndex { get; } + public Entity Entity { get { return World.Entities[EntityIndex]; } } + public bool InPVS { get { return Entity != null && Entity.InPVS; } } + + public UserInfo Info { get; set; } + + public IPlayerPropertyMonitor Position { get; } + public IPlayerPropertyMonitor Team { get; } + public IPlayerPropertyMonitor Class { get; } + public IPlayerPropertyMonitor IsDead { get; } + public IPlayerPropertyMonitor Health { get; } + public IPlayerPropertyMonitor MaxHealth { get; } + public IPlayerPropertyMonitor MaxBuffedHealth { get; } + public IPlayerPropertyMonitor Ping { get; } + public IPlayerPropertyMonitor Score { get; } + public IPlayerPropertyMonitor Deaths { get; } + public IPlayerPropertyMonitor Connected { get; } + public IPlayerPropertyMonitor PlayerState { get; } + public IPlayerPropertyMonitor Damage { get; } + + public ImmutableArray> Weapons { get; } + + SingleEvent> m_EnteredPVS { get; } = new SingleEvent>(); + public event Action EnteredPVS + { + add + { + if (!m_EnteredPVS.Add(value)) + return; + + if (InPVS) + value?.Invoke(this); + } + remove { m_EnteredPVS.Remove(value); } + } + + public SingleEvent> LeftPVS { get; } = new SingleEvent>(); + public SingleEvent> PropertiesUpdated { get; } = new SingleEvent>(); + + public Player(UserInfo info, WorldState ws, uint entityIndex) + { + EntityIndex = entityIndex; + Info = info; + World = ws; + + #region Property Monitors + Position = new PlayerPositionPropertyMonitor(this); + + Team = new MultiPlayerPropertyMonitor(this, + new IPropertyMonitor[] { + new PlayerResourcePropertyMonitor("m_iTeam", this, o => (Team)Convert.ToInt32(o)), + new PlayerPropertyMonitor("DT_BaseEntity.m_iTeamNum", this, o => (Team)Convert.ToInt32(o)) + }); + + IsDead = new MultiPlayerPropertyMonitor(this, + new IPropertyMonitor[] { + new PlayerResourcePropertyMonitor("m_bAlive", this, o => Convert.ToInt32(o) == 0), + new PlayerPropertyMonitor("DT_BasePlayer.m_lifeState", this, o => (LifeState)Convert.ToInt32(o) != LifeState.Alive) + }); + + Health = new MultiPlayerPropertyMonitor(this, + new IPropertyMonitor[] { + new PlayerResourcePropertyMonitor("m_iHealth", this, o => Convert.ToInt32(o)), + new PlayerPropertyMonitor("DT_BasePlayer.m_iHealth", this, o => Convert.ToInt32(o)), + }); + + Class = new MultiPlayerPropertyMonitor(this, + new IPropertyMonitor[] { + new PlayerResourcePropertyMonitor("m_iPlayerClass", this, o => (Class)Convert.ToInt32(o)), + new PlayerPropertyMonitor("DT_TFPlayerClassShared.m_iClass", this, o => (Class)Convert.ToInt32(o)) + }); + + PlayerState = new PlayerPropertyMonitor("DT_TFPlayerShared.m_nPlayerState", this, o => (PlayerState)(uint)(o)); + MaxHealth = new PlayerResourcePropertyMonitor("m_iMaxHealth", this, o => (uint)o); + MaxBuffedHealth = new PlayerResourcePropertyMonitor("m_iMaxBuffedHealth", this, o => (uint)o); + Ping = new PlayerResourcePropertyMonitor("m_iPing", this, o => (uint)o); + Score = new PlayerResourcePropertyMonitor("m_iScore", this, o => (int)o); + Deaths = new PlayerResourcePropertyMonitor("m_iDeaths", this, o => (int)o); + Connected = new PlayerResourcePropertyMonitor("m_bConnected", this, o => (uint)o != 0); + Damage = new PlayerResourcePropertyMonitor("m_iDamage", this, o => (uint)o); + + // weapons + { + IPlayerPropertyMonitor[] array = new IPlayerPropertyMonitor[48]; + for (int i = 0; i < 48; i++) + array[i] = new PlayerPropertyMonitor(string.Format("m_hMyWeapons.{0:D3}", i), this, o => new EHandle(ws, (uint)o)); + Weapons = ImmutableArray.Create(array); + } + #endregion + + World.Listeners.EntityEnteredPVS.Add(Listeners_EntityEnteredPVS); + World.Listeners.EntityLeftPVS.Add(Listeners_EntityLeftPVS); + + if (InPVS) + Listeners_EntityEnteredPVS(Entity); + } + + private void Listeners_EntityLeftPVS(Entity e) + { + Debug.Assert(e != null); + if (e != Entity) + return; + Debug.Assert(ReferenceEquals(e, Entity)); + + e.PropertiesUpdated.Remove(Entity_PropertiesUpdated); + + LeftPVS.Invoke(this); + } + + private void Listeners_EntityEnteredPVS(Entity e) + { + Debug.Assert(e != null); + if (e != Entity) + return; + Debug.Assert(ReferenceEquals(e, Entity)); + + e.PropertiesUpdated.Add(Entity_PropertiesUpdated); + + m_EnteredPVS?.Invoke(this); + } + + private void Entity_PropertiesUpdated(IPropertySet e) + { + Debug.Assert(ReferenceEquals(e, Entity)); + PropertiesUpdated.Invoke(this); + } + + public override string ToString() + { + return string.Format("\"{0}\": {1}", Info.Name, Info.GUID); + } + + [DebuggerDisplay("{Value}")] + class PlayerPropertyMonitor : IPlayerPropertyMonitor + { + bool m_ValueChanged = false; + public T Value { get; private set; } + object IPropertyMonitor.Value { get { return Value; } } + public SendProp Property { get; private set; } + + public string PropertyName { get; } + public Player Player { get; } + public Entity Entity { get { return Player.Entity; } } + + object DebugValue + { + get + { + SendProp prop = Player.Entity.Properties.SingleOrDefault(p => p.Definition.FullName == PropertyName); + + if (prop != null) + return Decoder(prop.Value); + else + return null; + } + } + + Func Decoder { get; } + + SingleEvent> IPropertyMonitor.ValueChanged { get; } = new SingleEvent>(); + SingleEvent>> IPropertyMonitor.ValueChanged { get; } = new SingleEvent>>(); + SingleEvent> IEntityPropertyMonitor.ValueChanged { get; } = new SingleEvent>(); + SingleEvent>> IEntityPropertyMonitor.ValueChanged { get; } = new SingleEvent>>(); + SingleEvent> IPlayerPropertyMonitor.ValueChanged { get; } = new SingleEvent>(); + public SingleEvent>> ValueChanged { get; } = new SingleEvent>>(); + + public PlayerPropertyMonitor(string propertyName, Player player, Func decoder) + { + ValueChanged.Add((self) => ((IPropertyMonitor)self).ValueChanged.Invoke(self)); + ValueChanged.Add((self) => ((IPropertyMonitor)self).ValueChanged.Invoke(self)); + ValueChanged.Add((self) => ((IEntityPropertyMonitor)self).ValueChanged.Invoke(self)); + ValueChanged.Add((self) => ((IEntityPropertyMonitor)self).ValueChanged.Invoke(self)); + ValueChanged.Add((self) => ((IPlayerPropertyMonitor)self).ValueChanged.Invoke(self)); + + Player = player; + PropertyName = propertyName; + Decoder = decoder; + + player.EnteredPVS += Player_EnteredPVS; + player.LeftPVS.Add(Player_LeftPVS); + player.PropertiesUpdated.Add(Player_PropertiesUpdated); + } + + private void Player_PropertiesUpdated(Player p) + { + if (m_ValueChanged) + { + ValueChanged.Invoke(this); + m_ValueChanged = false; + } + } + + private void Player_EnteredPVS(Player p) + { + Entity e = p.Entity; + e.PropertyAdded.Add(Entity_PropertyAdded); + + foreach (SendProp prop in e.Properties) + Entity_PropertyAdded(prop); + } + + private void Entity_PropertyAdded(SendProp prop) + { + if (prop.Definition.FullName == PropertyName) + { + Property = prop; + + if (prop.ValueChanged.Add(Prop_ValueChanged)) + { + // First add only + if (prop.Value != null) + Prop_ValueChanged(prop); + } + } + } + + private void Prop_ValueChanged(SendProp prop) + { + Debug.Assert(ReferenceEquals(prop.Entity, Entity)); + Debug.Assert((!Entity.InPVS && Property == null) || prop == Property); + var newValue = Decoder(prop.Value); + //Debug.Assert(Value?.Equals(newValue) != true); + Value = newValue; + m_ValueChanged = true; + } + + private void Player_LeftPVS(Player p) + { + p.Entity.PropertyAdded.Remove(Entity_PropertyAdded); + Property = null; + } + } + + [DebuggerDisplay("{Value}")] + class PlayerPositionPropertyMonitor : IPlayerPropertyMonitor + { + readonly Vector m_Value = new Vector(); + public Vector Value { get { return m_Value.Clone(); } } + object IPropertyMonitor.Value { get { return Value; } } + public SendProp Property { get { return null; } } + + public Player Player { get; } + public Entity Entity { get { return Player.Entity; } } + public string PropertyName { get { return null; } } + + IPlayerPropertyMonitor LocalOriginXY { get; } + IPlayerPropertyMonitor LocalOriginZ { get; } + IPlayerPropertyMonitor NonLocalOriginXY { get; } + IPlayerPropertyMonitor NonLocalOriginZ { get; } + + SingleEvent> IPropertyMonitor.ValueChanged { get; } = new SingleEvent>(); + SingleEvent>> IPropertyMonitor.ValueChanged { get; } = new SingleEvent>>(); + SingleEvent> IEntityPropertyMonitor.ValueChanged { get; } = new SingleEvent>(); + SingleEvent>> IEntityPropertyMonitor.ValueChanged { get; } = new SingleEvent>>(); + SingleEvent> IPlayerPropertyMonitor.ValueChanged { get; } = new SingleEvent>(); + public SingleEvent>> ValueChanged { get; } = new SingleEvent>>(); + + bool m_PositionChanged = false; + + public PlayerPositionPropertyMonitor(Player player) + { + ValueChanged.Add(self => ((IPropertyMonitor)self).ValueChanged.Invoke(self)); + ValueChanged.Add(self => ((IPropertyMonitor)self).ValueChanged.Invoke(self)); + ValueChanged.Add(self => ((IEntityPropertyMonitor)self).ValueChanged.Invoke(self)); + ValueChanged.Add(self => ((IEntityPropertyMonitor)self).ValueChanged.Invoke(self)); + ValueChanged.Add(self => ((IPlayerPropertyMonitor)self).ValueChanged.Invoke(self)); + + Player = player; + + LocalOriginXY = new PlayerPropertyMonitor("DT_TFLocalPlayerExclusive.m_vecOrigin", Player, o => (Vector)o); + LocalOriginZ = new PlayerPropertyMonitor("DT_TFLocalPlayerExclusive.m_vecOrigin[2]", Player, o => (double)o); + NonLocalOriginXY = new PlayerPropertyMonitor("DT_TFNonLocalPlayerExclusive.m_vecOrigin", Player, o => (Vector)o); + NonLocalOriginZ = new PlayerPropertyMonitor("DT_TFNonLocalPlayerExclusive.m_vecOrigin[2]", Player, o => (double)o); + + LocalOriginXY.ValueChanged.Add(OriginXY_ValueChanged); + LocalOriginZ.ValueChanged.Add(OriginZ_ValueChanged); + NonLocalOriginXY.ValueChanged.Add(OriginXY_ValueChanged); + NonLocalOriginZ.ValueChanged.Add(OriginZ_ValueChanged); + + Player.PropertiesUpdated.Add(Player_PropertiesUpdated); + } + + private void OriginZ_ValueChanged(IPlayerPropertyMonitor z) + { + m_Value.Z = z.Value; + m_PositionChanged = true; + } + private void OriginXY_ValueChanged(IPlayerPropertyMonitor xy) + { + var value = xy.Value; + m_Value.X = value.X; + m_Value.Y = value.Y; + m_PositionChanged = true; + } + + private void Player_PropertiesUpdated(Player p) + { + Debug.Assert(Player == p); + if (m_PositionChanged) + { + ValueChanged.Invoke(this); + m_PositionChanged = false; + } + } + } + + [DebuggerDisplay("{Value}")] + class PlayerResourcePropertyMonitor : IPlayerPropertyMonitor + { + public Player Player { get; } + public Entity Entity { get { return Player.Entity; } } + + public string PropertyName { get; } + public SendProp Property { get { return InternalPropertyMonitor.Property; } } + Func Decoder { get; } + + public T Value { get { return InternalPropertyMonitor.Value; } } + object IPropertyMonitor.Value { get { return Value; } } + + Entity PlayerResourceEntity { get; } + + EntityPropertyMonitor InternalPropertyMonitor { get; } + + SingleEvent> IPropertyMonitor.ValueChanged { get; } = new SingleEvent>(); + SingleEvent>> IPropertyMonitor.ValueChanged { get; } = new SingleEvent>>(); + SingleEvent> IEntityPropertyMonitor.ValueChanged { get; } = new SingleEvent>(); + SingleEvent>> IEntityPropertyMonitor.ValueChanged { get; } = new SingleEvent>>(); + SingleEvent> IPlayerPropertyMonitor.ValueChanged { get; } = new SingleEvent>(); + public SingleEvent>> ValueChanged { get; } = new SingleEvent>>(); + + public PlayerResourcePropertyMonitor(string propertyName, Player player, Func decoder) + { + ValueChanged.Add(self => ((IPropertyMonitor)self).ValueChanged.Invoke(self)); + ValueChanged.Add(self => ((IPropertyMonitor)self).ValueChanged.Invoke(self)); + ValueChanged.Add(self => ((IEntityPropertyMonitor)self).ValueChanged.Invoke(self)); + ValueChanged.Add(self => ((IEntityPropertyMonitor)self).ValueChanged.Invoke(self)); + ValueChanged.Add(self => ((IPlayerPropertyMonitor)self).ValueChanged.Invoke(self)); + + PropertyName = propertyName; + Player = player; + Decoder = decoder; + + PlayerResourceEntity = player.World.Entities.Single(e => e?.Class.Classname == "CTFPlayerResource"); + + string specificProperty = string.Format("{0}.{1:D3}", PropertyName, Player.EntityIndex); + + var props = PlayerResourceEntity.Properties.Select(prop => prop.Definition.FullName.Remove(prop.Definition.FullName.Length - 4)) + .Except("m_iHealth") + .Except("m_iPing") + .Except("m_iScore") + .Except("m_iDeaths") + .Except("m_bConnected") + .Except("m_iTeam") + .Except("m_bAlive") + .Distinct(); + + InternalPropertyMonitor = new EntityPropertyMonitor(specificProperty, PlayerResourceEntity, Decoder); + InternalPropertyMonitor.ValueChanged.Add(InternalValueChanged); + } + + private void InternalValueChanged(IPropertyMonitor p) + { + Debug.Assert(InternalPropertyMonitor == p); + ValueChanged.Invoke(this); + } + } + + [DebuggerDisplay("{Value}")] + class MultiPlayerPropertyMonitor : MultiPropertyMonitor, IPlayerPropertyMonitor + { + public Player Player { get; } + public Entity Entity { get { return Player.Entity; } } + + SingleEvent> IEntityPropertyMonitor.ValueChanged { get; } = new SingleEvent>(); + SingleEvent>> IEntityPropertyMonitor.ValueChanged { get; } = new SingleEvent>>(); + SingleEvent> IPlayerPropertyMonitor.ValueChanged { get; } = new SingleEvent>(); + public new SingleEvent>> ValueChanged { get; } = new SingleEvent>>(); + + public MultiPlayerPropertyMonitor(Player p, IEnumerable> propertyMonitors) : base(propertyMonitors) + { + //ValueChanged.Add(self => ((IPropertyMonitor)self).ValueChanged.Invoke(self)); + //ValueChanged.Add(self => ((IPropertyMonitor)self).ValueChanged.Invoke(self)); + //ValueChanged.Add(self => ((IEntityPropertyMonitor)self).ValueChanged.Invoke(self)); + //ValueChanged.Add(self => ((IEntityPropertyMonitor)self).ValueChanged.Invoke(self)); + //ValueChanged.Add(self => ((IPlayerPropertyMonitor)self).ValueChanged.Invoke(self)); + + Player = p; + + IPropertyMonitor self = this; + self.ValueChanged.Add(s => ((IEntityPropertyMonitor)s).ValueChanged.Invoke(this)); + self.ValueChanged.Add(s => ((IEntityPropertyMonitor)s).ValueChanged.Invoke(this)); + self.ValueChanged.Add(s => ((IPlayerPropertyMonitor)s).ValueChanged.Invoke(this)); + self.ValueChanged.Add(s => ((IPlayerPropertyMonitor)s).ValueChanged.Invoke(this)); + } + } + } +} diff --git a/src/TF2Net/Data/PlayerState.cs b/src/TF2Net/Data/PlayerState.cs new file mode 100644 index 0000000..8f412e7 --- /dev/null +++ b/src/TF2Net/Data/PlayerState.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TF2Net.Data +{ + public enum PlayerState + { + /// + /// Happily running around in the game. + /// + Active = 0, + + /// + /// First entering the server (shows level intro screen). + /// + Welcome = 1, + + /// + /// Game observer mode. + /// + Observer = 2, + + /// + /// Player is dying. + /// + Dying = 3, + } +} diff --git a/src/TF2Net/Data/SendProp.cs b/src/TF2Net/Data/SendProp.cs new file mode 100644 index 0000000..d0adfef --- /dev/null +++ b/src/TF2Net/Data/SendProp.cs @@ -0,0 +1,103 @@ +using System; +using System.Diagnostics; +using System.Linq; +using TF2Net.Entities; + +namespace TF2Net.Data +{ + [DebuggerDisplay("{Definition,nq} :: {Value,nq}")] + public class SendProp : ICloneable, IDisposable + { + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + readonly IEntity m_Entity; + public IEntity Entity + { + get + { + CheckDisposed(); + return m_Entity; + } + } + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + readonly SendPropDefinition m_Definition; + public SendPropDefinition Definition + { + get + { + CheckDisposed(); + return m_Definition; + } + } + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + ulong m_LastChangedTick; + public ulong LastChangedTick + { + get + { + CheckDisposed(); + return m_LastChangedTick; + } + } + + public SingleEvent> ValueChanged { get; } = new SingleEvent>(); + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + object m_Value; + public object Value + { + get + { + CheckDisposed(); + return m_Value; + } + set + { + CheckDisposed(); + if (value?.GetHashCode() != m_Value?.GetHashCode() || !value.Equals(m_Value)) + { + Debug.Assert(value?.Equals(m_Value) != true); + m_Value = value; + m_LastChangedTick = Entity.World.Tick; + ValueChanged.Invoke(this); + } + } + } + + public SendProp(IEntity e, SendPropDefinition definition) + { + m_Entity = e; + m_Definition = definition; + } + + public SendProp Clone(IEntity forEnt) + { + CheckDisposed(); + SendProp cloned = new SendProp(forEnt, Definition); + cloned.m_Value = Value; + cloned.m_LastChangedTick = LastChangedTick; + return cloned; + } + public SendProp Clone() + { + CheckDisposed(); + return (SendProp)MemberwiseClone(); + } + object ICloneable.Clone() { return Clone(); } + + void CheckDisposed() + { + if (m_Disposed) + throw new ObjectDisposedException(nameof(SendProp)); + } + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + bool m_Disposed = false; + public void Dispose() + { + CheckDisposed(); + m_Disposed = true; + } + } +} diff --git a/src/TF2Net/Data/SendPropDefinition.cs b/src/TF2Net/Data/SendPropDefinition.cs new file mode 100644 index 0000000..2bb28c1 --- /dev/null +++ b/src/TF2Net/Data/SendPropDefinition.cs @@ -0,0 +1,344 @@ +using System; +using System.Diagnostics; +using System.Text; +using BitSet; + +namespace TF2Net.Data +{ + [DebuggerDisplay("{ToString(),nq}")] + public class SendPropDefinition : ICloneable + { + private SendPropDefinition() { } + public SendPropDefinition(SendTable parent) + { + Parent = parent; + } + public SendTable Parent { get; } + + public SendPropType Type { get; set; } + public string Name { get; set; } + public SendPropFlags Flags { get; set; } + + public string ExcludeName { get; set; } + + public int? ArrayElements { get; set; } + public SendPropDefinition ArrayProperty { get; set; } + + public double? LowValue { get; set; } + public double? HighValue { get; set; } + public ulong? BitCount { get; set; } + + // If we're SendPropType.Datatable + public SendTable Table { get; set; } + + public string FullName { get { return string.Format("{0}.{1}", Parent.NetTableName, Name); } } + + public object Decode(BitStream stream) + { + switch (Type) + { + case SendPropType.Int: return ReadInt(stream); + case SendPropType.Vector: return ReadVector(stream); + case SendPropType.Float: return ReadFloat(stream); + case SendPropType.String: return ReadString(stream); + case SendPropType.Array: return ReadArray(stream); + case SendPropType.VectorXY: return ReadVectorXY(stream); + + default: + throw new NotImplementedException(); + } + } + + object[] ReadArray(BitStream stream) + { + int maxElements = ArrayElements.Value; + byte numBits = 1; + while ((maxElements >>= 1) != 0) + numBits++; + + uint elementCount = stream.ReadUInt(numBits); + object[] retVal = new object[elementCount]; + + for (int i = 0; i < elementCount; i++) + retVal[i] = ArrayProperty.Decode(stream); + + return retVal; + } + + string ReadString(BitStream stream) + { + ulong chars = stream.ReadULong(9); + + byte[] raw = stream.ReadBytes(chars); + return Encoding.ASCII.GetString(raw); + } + + object ReadInt(BitStream stream) + { + if (Flags.HasFlag(SendPropFlags.VarInt)) + { + if (Flags.HasFlag(SendPropFlags.Unsigned)) + return stream.ReadVarUInt(); + else + return stream.ReadVarInt(); + } + else + { + if (Flags.HasFlag(SendPropFlags.Unsigned)) + return stream.ReadUInt((byte)BitCount.Value); + else + return stream.ReadInt((byte)BitCount.Value); + } + } + + double ReadBitCoord(BitStream stream, bool isIntegral, bool isLowPrecision) + { + double value = 0; + bool isNegative = false; + bool inBounds = stream.ReadBool(); + + if (isIntegral) + { + bool hasIntVal = stream.ReadBool(); + if (hasIntVal) + { + isNegative = stream.ReadBool(); + + if (inBounds) + value = stream.ReadULong(SourceConstants.COORD_INTEGER_BITS_MP) + 1; + else + { + value = stream.ReadULong(SourceConstants.COORD_INTEGER_BITS) + 1; + + if (value < (1 << SourceConstants.COORD_INTEGER_BITS_MP)) + throw new FormatException("Something's fishy..."); + } + } + } + else + { + bool hasIntVal = stream.ReadBool(); + isNegative = stream.ReadBool(); + + if (hasIntVal) + { + if (inBounds) + value = stream.ReadULong(SourceConstants.COORD_INTEGER_BITS_MP) + 1; + else + { + value = stream.ReadULong(SourceConstants.COORD_INTEGER_BITS) + 1; + + if (value < (1 << SourceConstants.COORD_INTEGER_BITS_MP)) + throw new FormatException("Something's fishy..."); + } + } + + var fractVal = stream.ReadULong(isLowPrecision ? (byte)SourceConstants.COORD_FRACTIONAL_BITS_MP_LOWPRECISION : (byte)SourceConstants.COORD_FRACTIONAL_BITS); + + value = value + fractVal * (isLowPrecision ? SourceConstants.COORD_RESOLUTION_LOWPRECISION : SourceConstants.COORD_RESOLUTION); + } + + if (isNegative) + value = -value; + + return value; + } + + bool ReadSpecialFloat(BitStream stream, out double retVal) + { + if (Flags.HasFlag(SendPropFlags.Coord)) + { + throw new NotImplementedException(); + //return true; + } + else if (Flags.HasFlag(SendPropFlags.CoordMP)) + { + retVal = ReadBitCoord(stream, false, false); + return true; + } + else if (Flags.HasFlag(SendPropFlags.CoordMPLowPrecision)) + { + retVal = ReadBitCoord(stream, false, true); + return true; + } + else if (Flags.HasFlag(SendPropFlags.CoordMPIntegral)) + { + retVal = ReadBitCoord(stream, true, false); + return true; + } + else if (Flags.HasFlag(SendPropFlags.NoScale)) + { + retVal = stream.ReadSingle(); + return true; + } + else if (Flags.HasFlag(SendPropFlags.Normal)) + { + throw new NotImplementedException(); + //return true; + } + + retVal = default(double); + return false; + } + + double ReadFloat(BitStream stream) + { + double retVal; + if (ReadSpecialFloat(stream, out retVal)) + return retVal; + + ulong raw = stream.ReadULong((byte)BitCount.Value); + double percentage = (double)raw / ((1UL << (byte)BitCount.Value) - 1); + retVal = LowValue.Value + (HighValue.Value - LowValue.Value) * percentage; + + return retVal; + } + + Vector ReadVector(BitStream stream) + { + Vector retVal = new Vector(); + + retVal.X = ReadFloat(stream); + retVal.Y = ReadFloat(stream); + + if (!Flags.HasFlag(SendPropFlags.Normal)) + retVal.Z = ReadFloat(stream); + else + { + throw new NotImplementedException(); + } + + return retVal; + } + + Vector ReadVectorXY(BitStream stream) + { + Vector retVal = new Vector(); + + retVal.X = ReadFloat(stream); + retVal.Y = ReadFloat(stream); + + return retVal; + } + + public override string ToString() + { + string bitCount = (BitCount.HasValue && BitCount.Value > 0) ? string.Format("[{0}]", BitCount.Value) : string.Empty; + + return string.Format("{0}{1} \"{2}\" ({3})", Type, bitCount, FullName, Flags); + } + + public SendPropDefinition Clone() + { + SendPropDefinition retVal = (SendPropDefinition)MemberwiseClone(); + return retVal; + } + object ICloneable.Clone() { return Clone(); } + } + public enum SendPropType + { + Int = 0, + Float, + Vector, + VectorXY, + String, + Array, + Datatable, + //Quaternion, + //Int64, + } + [Flags] + public enum SendPropFlags + { + /// + /// Unsigned integer data. + /// + Unsigned = (1 << 0), + + /// + /// If this is set, the float/vector is treated like a world coordinate. + /// Note that the bit count is ignored in this case. + /// + Coord = (1 << 1), + + /// + /// For floating point, don't scale into range, just take value as is. + /// + NoScale = (1 << 2), + + /// + /// For floating point, limit high value to range minus one bit unit + /// + RoundDown = (1 << 3), + + /// + /// For floating point, limit low value to range minus one bit unit + /// + RoundUp = (1 << 4), + + /// + /// If this is set, the vector is treated like a normal (only valid for vectors) + /// + Normal = (1 << 5), + + /// + /// This is an exclude prop (not excludED, but it points at another prop to be excluded). + /// + Exclude = (1 << 6), + + /// + /// Use XYZ/Exponent encoding for vectors. + /// + EncodeXYZE = (1 << 7), + + /// + /// This tells us that the property is inside an array, so it shouldn't be put into the + /// flattened property list. Its array will point at it when it needs to. + /// + InsideArray = (1 << 8), + + /// + /// Set for datatable props using one of the default datatable proxies like + /// SendProxy_DataTableToDataTable that always send the data to all clients. + /// + ProxyAlwaysYes = (1 << 9), + + /// + /// this is an often changed field, moved to head of sendtable so it gets a small index + /// + ChangesOften = (1 << 10), + + /// + /// Set automatically if SPROP_VECTORELEM is used. + /// + IsVectorElement = (1 << 11), + + /// + /// Set automatically if it's a datatable with an offset of 0 that doesn't change the pointer + /// (ie: for all automatically-chained base classes). + /// In this case, it can get rid of this SendPropDataTable altogether and spare the + /// trouble of walking the hierarchy more than necessary. + /// + Collapsible = (1 << 12), + + /// + /// Like SPROP_COORD, but special handling for multiplayer games + /// + CoordMP = (1 << 13), + + /// + /// Like SPROP_COORD, but special handling for multiplayer games where the fractional component only gets a 3 bits instead of 5 + /// + CoordMPLowPrecision = (1 << 14), + + /// + /// SPROP_COORD_MP, but coordinates are rounded to integral boundaries + /// + CoordMPIntegral = (1 << 15), + + /// + /// reuse existing flag so we don't break demo. note you want to include SPROP_UNSIGNED if needed, its more efficient + /// + VarInt = Normal, + } +} diff --git a/src/TF2Net/Data/SendTable.cs b/src/TF2Net/Data/SendTable.cs new file mode 100644 index 0000000..030a284 --- /dev/null +++ b/src/TF2Net/Data/SendTable.cs @@ -0,0 +1,169 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Linq; + +namespace TF2Net.Data +{ + [DebuggerDisplay("SendTable {NetTableName}, {Properties.Count} SendProps")] + public class SendTable + { + public SendTable() + { + m_FlattenedProps = new Lazy>( + () => ImmutableArray.Create(SetupFlatPropertyArray().ToArray())); + } + + /// + /// The name matched between client and server. + /// + public string NetTableName { get; set; } + + public IList Properties { get; set; } = new List(); + + private Lazy> m_FlattenedProps; + public ImmutableArray FlattenedProps { get { return m_FlattenedProps.Value; } } + + public bool Unknown1 { get; set; } + + IEnumerable Excludes + { + get + { + foreach (SendPropDefinition prop in Properties) + { + if (prop.Flags.HasFlag(SendPropFlags.Exclude)) + yield return prop; + else if (prop.Type == SendPropType.Datatable) + { + foreach (SendPropDefinition childExclude in prop.Table.Excludes) + yield return childExclude; + } + } + } + } + + IEnumerable Flatten(IEnumerable excludes) + { + var datatablesFirst = Properties.OrderByDescending(p => p.Type, + Comparer.Create((p1, p2) => + { + bool isDT1 = p1 == SendPropType.Datatable; + bool isDT2 = p2 == SendPropType.Datatable; + + if (isDT1 == isDT2) + return 0; + + if (isDT1) + return 1; + else if (isDT2) + return -1; + + throw new InvalidOperationException(); + })); + + foreach (SendPropDefinition prop in datatablesFirst) + { + if (excludes.Any(e => e.Name == prop.Name && e.ExcludeName == prop.Parent.NetTableName)) + continue; + + if (excludes.Contains(prop)) + continue; + + Debug.Assert(!prop.Flags.HasFlag(SendPropFlags.Exclude)); + + if (prop.Type == SendPropType.Datatable) + { + foreach (FlattenedProp childProp in prop.Table.Flatten(excludes)) + { + childProp.FullName = childProp.FullName.Insert(0, NetTableName + '.'); + yield return childProp; + } + } + else + { + FlattenedProp flatProp = new FlattenedProp(); + flatProp.Property = prop; + flatProp.FullName = flatProp.Property.Name.Insert(0, NetTableName + '.'); + yield return flatProp; + } + } + } + + List SetupFlatPropertyArray() + { + var excludes = Excludes; + + List props = new List(); + + SendTable_BuildHierarchy(excludes, props); + + SendTable_SortByPriority(props); + + return props; + } + + void SendTable_BuildHierarchy(IEnumerable excludes, List allProperties) + { + List localProperties = new List(); + + SendTable_BuildHierarchy_IterateProps(excludes, localProperties, allProperties); + + allProperties.AddRange(localProperties); + } + + IEnumerable TestSortedProps + { + get { return SetupFlatPropertyArray(); } + } + + void SendTable_SortByPriority(List props) + { + int start = 0; + for (int i = start; i < props.Count; i++) + { + if (props[i].Flags.HasFlag(SendPropFlags.ChangesOften)) + { + if (i != start) + { + var temp = props[i]; + props[i] = props[start]; + props[start] = temp; + } + + start++; + continue; + } + } + } + + void SendTable_BuildHierarchy_IterateProps(IEnumerable excludes, List localProperties, List childDTProperties) + { + foreach (var prop in Properties) + { + if (prop.Flags.HasFlag(SendPropFlags.Exclude) || excludes.Contains(prop)) + { + continue; + } + + if (excludes.Any(e => e.Name == prop.Name && e.ExcludeName == prop.Parent.NetTableName)) + continue; + + if (prop.Type == SendPropType.Datatable) + { + if (prop.Flags.HasFlag(SendPropFlags.Collapsible)) + prop.Table.SendTable_BuildHierarchy_IterateProps(excludes, localProperties, childDTProperties); + else + { + prop.Table.SendTable_BuildHierarchy(excludes, childDTProperties); + } + } + else + { + localProperties.Add(prop); + } + } + } + } +} diff --git a/src/TF2Net/Data/ServerClass.cs b/src/TF2Net/Data/ServerClass.cs new file mode 100644 index 0000000..a81d5d8 --- /dev/null +++ b/src/TF2Net/Data/ServerClass.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TF2Net.Data +{ + [DebuggerDisplay("{ToString(),nq}")] + public class ServerClass + { + public string Classname { get; set; } + public string DatatableName { get; set; } + + public override string ToString() + { + return string.Format("{0} ({1})", Classname, DatatableName); + } + + public bool Equals(ServerClass other) + { + return ( + Classname == other.Classname && + DatatableName == other.DatatableName); + } + public override bool Equals(object obj) + { + ServerClass cast = obj as ServerClass; + return (cast != null ? Equals(cast) : false); + } + public override int GetHashCode() + { + return unchecked(Classname.GetHashCode() + DatatableName.GetHashCode()); + } + } +} diff --git a/src/TF2Net/Data/ServerInfo.cs b/src/TF2Net/Data/ServerInfo.cs new file mode 100644 index 0000000..1754e5f --- /dev/null +++ b/src/TF2Net/Data/ServerInfo.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TF2Net.Data +{ + [DebuggerDisplay("{Hostname}: {MapName}")] + public class ServerInfo + { + /// + /// protocol version + /// + public short Protocol { get; set; } + + /// + /// number of changelevels since server start + /// + public int ServerCount { get; set; } + + /// + /// dedicated server? + /// + public bool IsDedicated { get; set; } + + /// + /// HLTV server? + /// + public bool IsHLTV { get; set; } + + public enum OperatingSystem + { + Unknown, + + Linux, + Windows, + } + public OperatingSystem OS { get; set; } + + /// + /// server map CRC + /// + //public uint MapCRC { get; set; } + + /// + /// client.dll CRC server is using + /// + public uint ClientCRC { get; set; } + + /// + /// max number of clients on server + /// + public byte MaxClients { get; set; } + + /// + /// max number of server classes + /// + public ushort MaxClasses { get; set; } + + /// + /// our client slot number + /// + public int PlayerSlot { get; set; } + + /// + /// server tick interval + /// + public double TickInterval { get; set; } + + /// + /// game directory eg "tf2" + /// + public string GameDirectory { get; set; } + + public string MapName { get; set; } + + /// + /// Current skybox name + /// + public string SkyName { get; set; } + + /// + /// Server name + /// + public string Hostname { get; set; } + } +} diff --git a/src/TF2Net/Data/SignonState.cs b/src/TF2Net/Data/SignonState.cs new file mode 100644 index 0000000..146a9ca --- /dev/null +++ b/src/TF2Net/Data/SignonState.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TF2Net.Data +{ + public class SignonState + { + public ConnectionState State { get; set; } + public int SpawnCount { get; set; } + } +} diff --git a/src/TF2Net/Data/SourceConstants.cs b/src/TF2Net/Data/SourceConstants.cs new file mode 100644 index 0000000..eb7f5df --- /dev/null +++ b/src/TF2Net/Data/SourceConstants.cs @@ -0,0 +1,52 @@ +namespace TF2Net.Data +{ + public static class SourceConstants + { + public const int MAX_OSPATH = 260; + + internal const int NETMSG_TYPE_BITS = 6; + + internal const int EVENT_INDEX_BITS = 8; + internal const int MAX_EVENT_BITS = 9; + + internal const int NET_MAX_PAYLOAD_BITS = 17; + internal const int NET_MAX_PAYLOAD = (1 << NET_MAX_PAYLOAD_BITS); + + internal const int MAX_DECAL_INDEX_BITS = 9; + internal const int MAX_EDICT_BITS = 11; + internal const int MAX_EDICTS = (1 << MAX_EDICT_BITS); + + internal const int MAX_USER_MSG_LENGTH_BITS = 11; + internal const int MAX_USER_MSG_LENGTH = (1 << MAX_USER_MSG_LENGTH_BITS); + + internal const int MAX_ENTITY_MSG_LENGTH_BITS = 11; + internal const int MAX_ENTITY_MSG_LENGTH = (1 << MAX_ENTITY_MSG_LENGTH_BITS); + + internal const int MAX_SERVER_CLASS_BITS = 9; + internal const int MAX_SERVER_CLASSES = (1 << MAX_SERVER_CLASS_BITS); + + internal const int MAX_SOUND_INDEX_BITS = 14; + + internal const int SP_MODEL_INDEX_BITS = 11; + + internal const int NUM_NETWORKED_EHANDLE_SERIAL_NUMBER_BITS = 10; + internal const int NUM_NETWORKED_EHANDLE_BITS = (MAX_EDICT_BITS + NUM_NETWORKED_EHANDLE_SERIAL_NUMBER_BITS); + internal const int INVALID_NETWORKED_EHANDLE_VALUE = ((1 << NUM_NETWORKED_EHANDLE_BITS) - 1); + + internal const int MAX_DATATABLES = 1024; + internal const int MAX_DATATABLE_PROPS = 4096; + + internal const int COORD_INTEGER_BITS = 14; + internal const int COORD_FRACTIONAL_BITS = 5; + internal const int COORD_DENOMINATOR = 1 << COORD_FRACTIONAL_BITS; + internal const double COORD_RESOLUTION = 1.0 / COORD_DENOMINATOR; + + internal const int COORD_INTEGER_BITS_MP = 11; + internal const int COORD_FRACTIONAL_BITS_MP_LOWPRECISION = 3; + internal const int COORD_DENOMINATOR_LOWPRECISION = 1 << COORD_FRACTIONAL_BITS_MP_LOWPRECISION; + internal const double COORD_RESOLUTION_LOWPRECISION = 1.0 / COORD_DENOMINATOR_LOWPRECISION; + + internal const int SPROP_NUMFLAGBITS_NETWORKED = 16; + internal const int SPROP_NUMFLAGBITS = 17; + } +} diff --git a/src/TF2Net/Data/StringTable.cs b/src/TF2Net/Data/StringTable.cs new file mode 100644 index 0000000..0cc9b01 --- /dev/null +++ b/src/TF2Net/Data/StringTable.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TF2Net.Data +{ + [DebuggerDisplay("Stringtable: {TableName} ({Entries.Count,nq}/{MaxEntries,nq})")] + public class StringTable : IEnumerable + { + public WorldState World { get; } + + public string TableName { get; } + + public ushort MaxEntries { get; } + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + readonly SortedAutoList m_Entries; + public IReadOnlyList Entries { get { return m_Entries; } } + + public ushort? UserDataSize { get; } + public byte? UserDataSizeBits { get; } + + public SingleEvent> StringTableUpdated { get; } = new SingleEvent>(); + + public StringTable(WorldState ws, string tableName, ushort maxEntries, ushort? userDataSize, byte? userDataSizeBits) + { + World = ws; + TableName = tableName; + MaxEntries = maxEntries; + UserDataSize = userDataSize; + UserDataSizeBits = userDataSizeBits; + + m_Entries = new SortedAutoList( + Comparer.Create( + (lhs, rhs) => + { + Debug.Assert(lhs.ID != rhs.ID); + + return Comparer.Default.Compare(lhs.ID, rhs.ID); + })); + } + + public void Add(StringTableEntry entry) + { + Debug.Assert(entry.Table == this); + m_Entries.Add(entry); + entry.EntryChanged.Add(Entry_EntryChanged); + } + + private void Entry_EntryChanged(StringTableEntry entry) + { + StringTableUpdated.Invoke(this); + } + + private class SortedAutoList : SortedSet, IList, IReadOnlyList + { + public T this[int index] + { + get + { + return this.ElementAt(index); + } + set + { + Remove(this.ElementAt(index)); + Add(value); + } + } + + public int IndexOf(T item) + { + throw new NotSupportedException(); + } + + public void Insert(int index, T item) + { + throw new NotSupportedException(); + } + + public void RemoveAt(int index) + { + Remove(this.ElementAt(index)); + } + + public SortedAutoList(IComparer comparer) : base(comparer) { } + } + + public IEnumerator GetEnumerator() + { + return Entries.GetEnumerator(); + } + IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } + } +} diff --git a/src/TF2Net/Data/StringTableEntry.cs b/src/TF2Net/Data/StringTableEntry.cs new file mode 100644 index 0000000..36af9ba --- /dev/null +++ b/src/TF2Net/Data/StringTableEntry.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using BitSet; + +namespace TF2Net.Data +{ + [DebuggerDisplay("{ID,nq}: {Value}")] + public class StringTableEntry + { + public StringTable Table { get; } + + public SingleEvent> EntryChanged { get; } = new SingleEvent>(); + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + ushort m_ID; + public ushort ID + { + get { return m_ID; } + set + { + if (m_ID != value) + { + m_ID = value; + EntryChanged?.Invoke(this); + } + } + } + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + string m_Value; + public string Value + { + get { return m_Value; } + set + { + if (m_Value != value) + { + m_Value = value; + EntryChanged?.Invoke(this); + } + } + } + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + BitStream m_UserData; + public BitStream UserData + { + get { return m_UserData?.Clone(); } + set + { + if (m_UserData != value) + { + m_UserData = value; + EntryChanged?.Invoke(this); + } + } + } + + public StringTableEntry(StringTable table) + { + Table = table; + } + } +} diff --git a/src/TF2Net/Data/Team.cs b/src/TF2Net/Data/Team.cs new file mode 100644 index 0000000..39938b4 --- /dev/null +++ b/src/TF2Net/Data/Team.cs @@ -0,0 +1,11 @@ +namespace TF2Net.Data +{ + public enum Team + { + Invalid = -1, + Unassigned = 0, + Spectator = 1, + Red = 2, + Blue = 3, + } +} diff --git a/src/TF2Net/Data/UserInfo.cs b/src/TF2Net/Data/UserInfo.cs new file mode 100644 index 0000000..836bf3f --- /dev/null +++ b/src/TF2Net/Data/UserInfo.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using BitSet; + +namespace TF2Net.Data +{ + public class UserInfo + { + public string Name { get; set; } + public int? UserID { get; set; } + + public string GUID { get; set; } + + public uint? FriendsID { get; set; } + public string FriendsName { get; set; } + + public bool? IsFakePlayer { get; set; } + public bool? IsHLTV { get; set; } + + public uint?[] CustomFiles { get; } = new uint?[4]; + + public uint? FilesDownloaded { get; set; } + + public UserInfo(BitStream stream) + { + Name = Encoding.ASCII.GetString(stream.ReadBytes(32)).TrimEnd('\0'); + + UserID = stream.ReadInt(); + + GUID = Encoding.ASCII.GetString(stream.ReadBytes(33)).TrimEnd('\0'); + + FriendsID = stream.ReadUInt(); + + FriendsName = Encoding.ASCII.GetString(stream.ReadBytes(32)).TrimEnd('\0'); + + IsFakePlayer = stream.ReadByte() > 0 ? true : false; + IsHLTV = stream.ReadByte() > 0 ? true : false; + + for (byte i = 0; i < 4; i++) + CustomFiles[i] = stream.ReadUInt(); + + FilesDownloaded = stream.ReadByte(); + } + } +} diff --git a/src/TF2Net/Data/UserMessageType.cs b/src/TF2Net/Data/UserMessageType.cs new file mode 100644 index 0000000..1fb8a59 --- /dev/null +++ b/src/TF2Net/Data/UserMessageType.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TF2Net.Data +{ + public enum UserMessageType + { + Geiger = 0, + Train = 1, + HudText = 2, + SayText = 3, + SayText2 = 4, + TextMsg = 5, + ResetHUD = 6, + GameTitle = 7, + ItemPickup = 8, + ShowMenu = 9, + Shake = 10, + + HudNotifyCustom = 27, + + BreakModel = 41, + CheapBreakModel = 42, + + MVMResetPlayerStats = 57, + } +} diff --git a/src/TF2Net/Data/Vector.cs b/src/TF2Net/Data/Vector.cs new file mode 100644 index 0000000..898dbbd --- /dev/null +++ b/src/TF2Net/Data/Vector.cs @@ -0,0 +1,75 @@ +using System; +using System.Diagnostics; + +namespace TF2Net.Data +{ + [DebuggerDisplay("{ToString(),nq}")] + public class Vector : IReadOnlyVector, ICloneable + { + public double X { get; set; } + public double Y { get; set; } + public double Z { get; set; } + + public Vector() { } + public Vector(double x, double y, double z = 0) + { + X = x; + Y = y; + Z = z; + } + public Vector(double[] xyz) + { + if (xyz == null) + throw new ArgumentNullException(nameof(xyz)); + if (xyz.Length != 3) + throw new ArgumentException("Array is not of length 3", nameof(xyz)); + + X = xyz[0]; + Y = xyz[1]; + Z = xyz[2]; + } + public Vector(IReadOnlyVector v) + { + X = v.X; + Y = v.Y; + Z = v.Z; + } + + public double this[int i] + { + get + { + switch (i) + { + case 0: return X; + case 1: return Y; + case 2: return Z; + } + + throw new ArgumentOutOfRangeException(nameof(i)); + } + set + { + switch (i) + { + case 0: X = value; return; + case 1: Y = value; return; + case 2: Z = value; return; + } + + throw new ArgumentOutOfRangeException(nameof(i)); + } + } + + public override string ToString() + { + return string.Format("{0}: ({1} {2} {3})", nameof(Vector), X, Y, Z); + } + + public Vector Clone() + { + return (Vector)MemberwiseClone(); + } + object ICloneable.Clone() { return Clone(); } + } +} diff --git a/src/TF2Net/Data/WeaponType.cs b/src/TF2Net/Data/WeaponType.cs new file mode 100644 index 0000000..efdd95f --- /dev/null +++ b/src/TF2Net/Data/WeaponType.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TF2Net.Data +{ + public enum WeaponType + { + TF_WEAPON_NONE = 0, + TF_WEAPON_BAT, + TF_WEAPON_BAT_WOOD, + TF_WEAPON_BOTTLE, + TF_WEAPON_FIREAXE, + TF_WEAPON_CLUB, + TF_WEAPON_CROWBAR, + TF_WEAPON_KNIFE, + TF_WEAPON_FISTS, + TF_WEAPON_SHOVEL, + TF_WEAPON_WRENCH, + TF_WEAPON_BONESAW, + TF_WEAPON_SHOTGUN_PRIMARY, + TF_WEAPON_SHOTGUN_SOLDIER, + TF_WEAPON_SHOTGUN_HWG, + TF_WEAPON_SHOTGUN_PYRO, + TF_WEAPON_SCATTERGUN, + TF_WEAPON_SNIPERRIFLE, + TF_WEAPON_MINIGUN, + TF_WEAPON_SMG, + TF_WEAPON_SYRINGEGUN_MEDIC, + TF_WEAPON_TRANQ, + TF_WEAPON_ROCKETLAUNCHER, + TF_WEAPON_GRENADELAUNCHER, + TF_WEAPON_PIPEBOMBLAUNCHER, + TF_WEAPON_FLAMETHROWER, + TF_WEAPON_GRENADE_NORMAL, + TF_WEAPON_GRENADE_CONCUSSION, + TF_WEAPON_GRENADE_NAIL, + TF_WEAPON_GRENADE_MIRV, + TF_WEAPON_GRENADE_MIRV_DEMOMAN, + TF_WEAPON_GRENADE_NAPALM, + TF_WEAPON_GRENADE_GAS, + TF_WEAPON_GRENADE_EMP, + TF_WEAPON_GRENADE_CALTROP, + TF_WEAPON_GRENADE_PIPEBOMB, + TF_WEAPON_GRENADE_SMOKE_BOMB, + TF_WEAPON_GRENADE_HEAL, + TF_WEAPON_GRENADE_STUNBALL, + TF_WEAPON_GRENADE_JAR, + TF_WEAPON_GRENADE_JAR_MILK, + TF_WEAPON_PISTOL, + TF_WEAPON_PISTOL_SCOUT, + TF_WEAPON_REVOLVER, + TF_WEAPON_NAILGUN, + TF_WEAPON_PDA, + TF_WEAPON_PDA_ENGINEER_BUILD, + TF_WEAPON_PDA_ENGINEER_DESTROY, + TF_WEAPON_PDA_SPY, + TF_WEAPON_BUILDER, + TF_WEAPON_MEDIGUN, + TF_WEAPON_GRENADE_MIRVBOMB, + TF_WEAPON_FLAMETHROWER_ROCKET, + TF_WEAPON_GRENADE_DEMOMAN, + TF_WEAPON_SENTRY_BULLET, + TF_WEAPON_SENTRY_ROCKET, + TF_WEAPON_DISPENSER, + TF_WEAPON_INVIS, + TF_WEAPON_FLAREGUN, + TF_WEAPON_LUNCHBOX, + TF_WEAPON_JAR, + TF_WEAPON_COMPOUND_BOW, + TF_WEAPON_BUFF_ITEM, + TF_WEAPON_PUMPKIN_BOMB, + TF_WEAPON_SWORD, + TF_WEAPON_DIRECTHIT, + TF_WEAPON_LIFELINE, + TF_WEAPON_LASER_POINTER, + TF_WEAPON_DISPENSER_GUN, + TF_WEAPON_SENTRY_REVENGE, + TF_WEAPON_JAR_MILK, + TF_WEAPON_HANDGUN_SCOUT_PRIMARY, + TF_WEAPON_BAT_FISH, + TF_WEAPON_CROSSBOW, + TF_WEAPON_STICKBOMB, + TF_WEAPON_HANDGUN_SCOUT_SEC, + TF_WEAPON_SODA_POPPER, + TF_WEAPON_SNIPERRIFLE_DECAP, + TF_WEAPON_RAYGUN, + TF_WEAPON_PARTICLE_CANNON, + TF_WEAPON_MECHANICAL_ARM, + TF_WEAPON_DRG_POMSON, + TF_WEAPON_BAT_GIFTWRAP, + TF_WEAPON_GRENADE_ORNAMENT, + TF_WEAPON_RAYGUN_REVENGE, + TF_WEAPON_PEP_BRAWLER_BLASTER, + TF_WEAPON_CLEAVER, + TF_WEAPON_GRENADE_CLEAVER, + TF_WEAPON_STICKY_BALL_LAUNCHER, + TF_WEAPON_GRENADE_STICKY_BALL, + TF_WEAPON_SHOTGUN_BUILDING_RESCUE, + TF_WEAPON_CANNON, + TF_WEAPON_THROWABLE, + TF_WEAPON_GRENADE_THROWABLE, + TF_WEAPON_PDA_SPY_BUILD, + TF_WEAPON_GRENADE_WATERBALLOON, + TF_WEAPON_HARVESTER_SAW, + TF_WEAPON_SPELLBOOK, + TF_WEAPON_SPELLBOOK_PROJECTILE, + TF_WEAPON_SNIPERRIFLE_CLASSIC, + } +} diff --git a/src/TF2Net/Data/WorldState.cs b/src/TF2Net/Data/WorldState.cs new file mode 100644 index 0000000..f6104ce --- /dev/null +++ b/src/TF2Net/Data/WorldState.cs @@ -0,0 +1,150 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using BitSet; +using TF2Net.Entities; + +namespace TF2Net.Data +{ + public class WorldState + { + WorldEvents m_Listeners; + public WorldEvents Listeners + { + get { return m_Listeners; } + set + { + Debug.Assert(m_Listeners == null); + + m_Listeners = value; + RegisterEventHandlers(); + } + } + + public ulong? EndTick { get; set; } + public ulong BaseTick { get; set; } = 0; + public ulong Tick { get; set; } + + public double LastFrameTime { get; set; } + public double LastFrameTimeStdDev { get; set; } + + public SignonState SignonState { get; set; } + + public ServerInfo ServerInfo { get; set; } + + public Entity[] Entities { get; } = new Entity[SourceConstants.MAX_EDICTS]; + public IEnumerable EntitiesInPVS + { + get + { + for (int i = 0; i < SourceConstants.MAX_EDICTS; i++) + { + Entity e = Entities[i]; + if (e?.InPVS == true) + yield return e; + } + } + } + + public IList StringTables { get; } = new List(); + + public IDictionary ConVars { get; } = new Dictionary(StringComparer.OrdinalIgnoreCase); + + public ushort? ViewEntity { get; set; } + + public IList ServerClasses { get; set; } + public IList SendTables { get; set; } + public byte ClassBits { get { return (byte)Math.Ceiling(Math.Log(ServerClasses.Count, 2)); } } + + public IList EventDeclarations { get; set; } + + public IEnumerable> StaticBaselines + { + get + { + return StringTables.Single(st => st.TableName == "instancebaseline") + .Entries.Select(e => new KeyValuePair(ServerClasses[int.Parse(e.Value)], e.UserData)); + } + } + public IList[][] InstanceBaselines { get; } = new IList[2][] + { + new IList[SourceConstants.MAX_EDICTS], + new IList[SourceConstants.MAX_EDICTS], + }; + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + readonly List m_CachedPlayers = new List(); + public IEnumerable Players + { + get + { + if (SignonState?.State != ConnectionState.Full) + yield break; + + IEnumerable table = StringTables.SingleOrDefault(st => st.TableName == "userinfo"); + if (table == null) + { + lock (m_CachedPlayers) + m_CachedPlayers.Clear(); + + yield break; + } + + List touched = new List(); + foreach (var user in table) + { + var localData = user.UserData; + if (localData == null) + continue; + Debug.Assert(localData.Cursor == 0); + + UserInfo decoded = new UserInfo(localData); + + Player existing; + lock (m_CachedPlayers) + existing = m_CachedPlayers.SingleOrDefault(p => p.Info.GUID == decoded.GUID); + + uint entityIndex = uint.Parse(user.Value) + 1; + + Existing: + if (existing != null) + { + Debug.Assert(entityIndex == existing.EntityIndex); + existing.Info = decoded; + touched.Add(existing); + yield return existing; + } + else + { + Player newPlayer; + lock (m_CachedPlayers) + { + // Check again + existing = m_CachedPlayers.SingleOrDefault(p => p.Info.GUID == decoded.GUID); + if (existing != null) + goto Existing; + else + { + newPlayer = new Player(decoded, this, entityIndex); + Listeners.PlayerAdded.Invoke(newPlayer); + m_CachedPlayers.Add(newPlayer); + touched.Add(newPlayer); + } + } + + yield return newPlayer; + } + } + + Console.WriteLine(touched); + } + } + + void RegisterEventHandlers() + { + if (Listeners == null) + throw new ArgumentNullException(nameof(Listeners)); + } + } +} diff --git a/src/TF2Net/Entities/Entity.cs b/src/TF2Net/Entities/Entity.cs new file mode 100644 index 0000000..41236eb --- /dev/null +++ b/src/TF2Net/Entities/Entity.cs @@ -0,0 +1,136 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using TF2Net.Data; +using TF2Net.Monitors; + +namespace TF2Net.Entities +{ + [DebuggerDisplay("{ToString(),nq}")] + public class Entity : IEntity, IDisposable, IEquatable + { + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + readonly WorldState m_World; + public WorldState World { get { CheckDisposed(); return m_World; } } + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + readonly ServerClass m_Class; + public ServerClass Class { get { CheckDisposed(); return m_Class; } } + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + readonly SendTable m_NetworkTable; + public SendTable NetworkTable { get { CheckDisposed(); return m_NetworkTable; } } + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + readonly List m_Properties = new List(); + public IReadOnlyList Properties { get { CheckDisposed(); return m_Properties; } } + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + readonly uint m_Index; + public uint Index { get { CheckDisposed(); return m_Index; } } + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + readonly uint m_SerialNumber; + public uint SerialNumber { get { CheckDisposed(); return m_SerialNumber; } } + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + bool m_InPVS; + public bool InPVS + { + get { CheckDisposed(); return m_InPVS; } + set + { + CheckDisposed(); + var oldValue = m_InPVS; + m_InPVS = value; + + if (value && !oldValue) + { + EnteredPVS.Invoke(this); + World.Listeners.EntityEnteredPVS.Invoke(this); + } + else if (!value && oldValue) + { + LeftPVS.Invoke(this); + World.Listeners.EntityLeftPVS.Invoke(this); + } + } + } + + public SingleEvent> EnteredPVS { get; } = new SingleEvent>(); + public SingleEvent> LeftPVS { get; } = new SingleEvent>(); + + public SingleEvent> PropertyAdded { get; } = new SingleEvent>(); + public SingleEvent> PropertiesUpdated { get; } = new SingleEvent>(); + + public IEntityPropertyMonitor Owner { get; } + public IEntityPropertyMonitor Team { get; } + + public Entity(WorldState ws, ServerClass sClass, SendTable table, uint index, uint serialNumber) + { + m_World = ws; + m_Class = sClass; + m_NetworkTable = table; + m_Index = index; + m_SerialNumber = serialNumber; + + Team = new EntityPropertyMonitor("DT_BaseEntity.m_iTeamNum", this, o => (Team)(int)o); + Owner = new EntityPropertyMonitor("DT_BaseEntity.m_hOwnerEntity", this, o => new EHandle(ws, (uint)o)); + } + + public void AddProperty(SendProp newProp) + { + CheckDisposed(); + Debug.Assert(!m_Properties.Any(p => ReferenceEquals(p.Definition, newProp.Definition))); + Debug.Assert(ReferenceEquals(newProp.Entity, this)); + + m_Properties.Add(newProp); + PropertyAdded.Invoke(newProp); + } + + public override string ToString() + { + CheckDisposed(); + return string.Format("{0}({1}): {2}", Index, SerialNumber, Class.Classname); + } + + public bool Equals(Entity other) + { + CheckDisposed(); + return ( + other?.Index == Index && + other.SerialNumber == SerialNumber); + } + public override bool Equals(object obj) + { + CheckDisposed(); + if (GetHashCode() != obj.GetHashCode()) + return false; + + return Equals(obj as Entity); + } + public override int GetHashCode() + { + CheckDisposed(); + return (int)(Index + (SerialNumber << SourceConstants.MAX_EDICT_BITS)); + } + + protected void CheckDisposed() + { + if (m_Disposed) + throw new ObjectDisposedException(nameof(Entity)); + } + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + bool m_Disposed = false; + public void Dispose() + { + CheckDisposed(); + m_Disposed = true; + + foreach (SendProp prop in m_Properties) + prop.Dispose(); + } + } +} diff --git a/src/TF2Net/Entities/EntityWrapper.cs b/src/TF2Net/Entities/EntityWrapper.cs new file mode 100644 index 0000000..9447a84 --- /dev/null +++ b/src/TF2Net/Entities/EntityWrapper.cs @@ -0,0 +1,31 @@ +using System; + +namespace TF2Net.Entities +{ + public abstract class BaseEntityWrapper + { + public IBaseEntity Entity { get; } + + public BaseEntityWrapper(IBaseEntity e, string className) + { + if (e.Class.Classname != className) + throw new ArgumentException(string.Format("Invalid entity class for this {0}", nameof(BaseEntityWrapper))); + + Entity = e; + } + } + + public abstract class AbstractEntityWrapper : BaseEntityWrapper + { + public new IEntity Entity { get { return (IEntity)base.Entity; } } + + public AbstractEntityWrapper(IEntity e, string className) : base(e, className) { } + } + + public abstract class EntityWrapper : AbstractEntityWrapper + { + public new Entity Entity { get { return (Entity)base.Entity; } } + + public EntityWrapper(Entity e, string className) : base(e, className) { } + } +} diff --git a/src/TF2Net/Entities/IEntity.cs b/src/TF2Net/Entities/IEntity.cs new file mode 100644 index 0000000..40fe83d --- /dev/null +++ b/src/TF2Net/Entities/IEntity.cs @@ -0,0 +1,12 @@ +using TF2Net.Data; + +namespace TF2Net.Entities +{ + public interface IBaseEntity : IStaticPropertySet + { + WorldState World { get; } + ServerClass Class { get; } + SendTable NetworkTable { get; } + } + public interface IEntity : IBaseEntity, IPropertySet { } +} diff --git a/src/TF2Net/Entities/IPropertySet.cs b/src/TF2Net/Entities/IPropertySet.cs new file mode 100644 index 0000000..03c4f2a --- /dev/null +++ b/src/TF2Net/Entities/IPropertySet.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using TF2Net.Data; + +namespace TF2Net.Entities +{ + public interface IStaticPropertySet + { + IReadOnlyList Properties { get; } + } + public interface IPropertySet : IStaticPropertySet + { + SingleEvent> PropertyAdded { get; } + SingleEvent> PropertiesUpdated { get; } + + void AddProperty(SendProp prop); + } + + [EditorBrowsable(EditorBrowsableState.Never)] + public static class IPropertySetExtensions + { + public static SendProp GetProperty(this IStaticPropertySet set, SendPropDefinition def) + { + var retVal = set.Properties.SingleOrDefault(x => x.Definition == def); + Debug.Assert(retVal == null || ReferenceEquals(retVal.Definition, def)); + Debug.Assert(retVal == null || ReferenceEquals(retVal.Entity, set)); + return retVal; + } + public static SendProp GetProperty(this IStaticPropertySet set, string propName) + { + var retVal = set.Properties.SingleOrDefault(x => x.Definition.FullName == propName); + return retVal; + } + } +} diff --git a/src/TF2Net/Entities/Pill.cs b/src/TF2Net/Entities/Pill.cs new file mode 100644 index 0000000..52f4ef5 --- /dev/null +++ b/src/TF2Net/Entities/Pill.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using TF2Net.Data; + +namespace TF2Net.Entities +{ + public class Pill : EntityWrapper + { + public const string CLASSNAME = "CTFGrenadePipebombProjectile"; + + public Pill(Entity e) : base(e, CLASSNAME) + { + } + } +} diff --git a/src/TF2Net/Entities/TFRocket.cs b/src/TF2Net/Entities/TFRocket.cs new file mode 100644 index 0000000..9dcea80 --- /dev/null +++ b/src/TF2Net/Entities/TFRocket.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using TF2Net.Data; +using TF2Net.Monitors; + +namespace TF2Net.Entities +{ + public class TFRocket : EntityWrapper, IEquatable, IEquatable + { + public IEntityPropertyMonitor Position { get; } + public IEntityPropertyMonitor Angle { get; } + public IEntityPropertyMonitor Team { get { return Entity.Team; } } + public IEntityPropertyMonitor Launcher { get; } + + public TFRocket(Entity e) : base(e, "CTFProjectile_Rocket") + { + Position = new EntityPropertyMonitor("DT_TFBaseRocket.m_vecOrigin", Entity, o => (Vector)o); + Angle = new EntityPropertyMonitor("DT_TFBaseRocket.m_angRotation", Entity, o => (Vector)o); + Launcher = new EntityPropertyMonitor("DT_TFBaseRocket.m_hLauncher", Entity, o => new EHandle(e.World, (uint)o)); + } + + public bool Equals(TFRocket other) + { + return Entity.Equals(other?.Entity); + } + public override bool Equals(object obj) + { + // Entity + { + Entity e = obj as Entity; + if (e != null) + return Equals(e); + } + + // TFRocket + { + TFRocket r = obj as TFRocket; + if (r != null) + return Equals(r); + } + + return false; + } + public override int GetHashCode() + { + return Entity.GetHashCode(); + } + + public bool Equals(Entity other) + { + return ((IEquatable)Entity).Equals(other); + } + } +} diff --git a/src/TF2Net/Entities/TempEntities/FireBullets.cs b/src/TF2Net/Entities/TempEntities/FireBullets.cs new file mode 100644 index 0000000..0b75325 --- /dev/null +++ b/src/TF2Net/Entities/TempEntities/FireBullets.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using TF2Net.Data; + +namespace TF2Net.Entities.TempEntities +{ + public class FireBullets : BaseEntityWrapper + { + public Player Player { get; } + public WeaponType Weapon { get; } + public IReadOnlyVector Origin { get; } + + public const string CLASSNAME = "CTEFireBullets"; + public FireBullets(IBaseEntity e) : base(e, CLASSNAME) + { + Origin = (Vector)e.GetProperty("DT_TEFireBullets.m_vecOrigin")?.Value ?? new Vector(); + + { + uint playerIndex = (uint)e.GetProperty("DT_TEFireBullets.m_iPlayer").Value; + Player = e.World.Players.ElementAt((int)playerIndex); + } + + Weapon = (WeaponType)(uint)e.GetProperty("DT_TEFireBullets.m_iWeaponID").Value; + } + } +} diff --git a/src/TF2Net/Entities/TempEntities/TFBlood.cs b/src/TF2Net/Entities/TempEntities/TFBlood.cs new file mode 100644 index 0000000..77c26f1 --- /dev/null +++ b/src/TF2Net/Entities/TempEntities/TFBlood.cs @@ -0,0 +1,27 @@ +using TF2Net.Data; + +namespace TF2Net.Entities.TempEntities +{ + public class TFBlood : BaseEntityWrapper + { + public IReadOnlyVector Origin { get; } + + public uint? TargetEntityIndex { get; } + public Entity TargetEntity { get { return TargetEntityIndex.HasValue ? Entity.World.Entities[TargetEntityIndex.Value] : null; } } + + public const string CLASSNAME = "CTETFBlood"; + public TFBlood(IBaseEntity e) : base(e, CLASSNAME) + { + { + Vector origin = new Vector(); + // "DT_TETFBlood.m_vecOrigin[0]" + origin.X = (double?)e.GetProperty("DT_TETFBlood.m_vecOrigin[0]")?.Value ?? 0; + origin.Y = (double?)e.GetProperty("DT_TETFBlood.m_vecOrigin[1]")?.Value ?? 0; + origin.Z = (double?)e.GetProperty("DT_TETFBlood.m_vecOrigin[2]")?.Value ?? 0; + Origin = origin; + } + + TargetEntityIndex = (uint?)e.GetProperty("DT_TETFBlood.entindex")?.Value; + } + } +} diff --git a/src/TF2Net/Entities/TempEntities/TFExplosion.cs b/src/TF2Net/Entities/TempEntities/TFExplosion.cs new file mode 100644 index 0000000..4749606 --- /dev/null +++ b/src/TF2Net/Entities/TempEntities/TFExplosion.cs @@ -0,0 +1,24 @@ +using TF2Net.Data; + +namespace TF2Net.Entities.TempEntities +{ + public class TFExplosion : BaseEntityWrapper + { + public IReadOnlyVector Origin { get; } + public IReadOnlyVector Normal { get; } + + public const string CLASSNAME = "CTETFExplosion"; + public TFExplosion(IBaseEntity e) : base(e, CLASSNAME) + { + { + Vector origin = new Vector(); + origin.X = (double?)e.GetProperty("DT_TETFExplosion.m_vecOrigin[0]")?.Value ?? 0; + origin.Y = (double?)e.GetProperty("DT_TETFExplosion.m_vecOrigin[1]")?.Value ?? 0; + origin.Z = (double?)e.GetProperty("DT_TETFExplosion.m_vecOrigin[2]")?.Value ?? 0; + Origin = origin; + } + + Normal = (Vector)e.GetProperty("DT_TETFExplosion.m_vecNormal").Value; + } + } +} diff --git a/src/TF2Net/Extensions/AutoDictionary.cs b/src/TF2Net/Extensions/AutoDictionary.cs new file mode 100644 index 0000000..35f1992 --- /dev/null +++ b/src/TF2Net/Extensions/AutoDictionary.cs @@ -0,0 +1,198 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; + +namespace TF2Net.Extensions +{ + class AutoDictionary : IDictionary, IReadOnlyDictionary + { + [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] + readonly List m_Contents = new List(); + + public TValue this[TKey key] + { + get + { + TValue outValue; + if (!TryGetValue(key, out outValue)) + throw new KeyNotFoundException(); + + return outValue; + } + set + { + for (uint i = 0; i < m_Contents.Count; i++) + { + TKey currentKey = m_KeySelector(m_Contents[(int)i]); + if (m_KeyComparer.Equals(currentKey, key)) + { + m_Contents[(int)i] = value; + return; + } + } + + Add(key, value); + } + } + + public int Count { get { return m_Contents.Count; } } + public bool IsReadOnly { get { return false; } } + public ICollection Keys { get { return new KeyCollection(m_Contents, m_KeySelector, m_KeyComparer); } } + public ICollection Values { get { return m_Contents; } } + + IEnumerable IReadOnlyDictionary.Keys { get { return m_Contents.Select(v => m_KeySelector(v)); } } + IEnumerable IReadOnlyDictionary.Values { get { return Values; } } + + readonly Func m_KeySelector; + readonly IEqualityComparer m_KeyComparer; + + public AutoDictionary(Func keySelector) : this(keySelector, EqualityComparer.Default) { } + public AutoDictionary(Func keySelector, IEqualityComparer keyComparer) + { + if (keySelector == null) + throw new ArgumentNullException(nameof(keySelector)); + if (keyComparer == null) + throw new ArgumentNullException(nameof(keyComparer)); + + m_KeySelector = keySelector; + m_KeyComparer = keyComparer; + } + + public void Add(KeyValuePair item) + { + Add(item.Key, item.Value); + } + + public void Add(TKey key, TValue value) + { + Debug.Assert(m_KeyComparer.Equals(key, m_KeySelector(value))); + Add(value); + } + + public void Add(TValue value) + { + m_Contents.Add(value); + } + + public void Clear() + { + m_Contents.Clear(); + } + + public bool Contains(KeyValuePair item) + { + throw new NotImplementedException(); + } + + public bool ContainsKey(TKey key) + { + return m_Contents.SingleOrDefault(v => m_KeyComparer.Equals(key, m_KeySelector(v))) != null; + } + + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + foreach (var kvPair in this) + array[arrayIndex++] = kvPair; + } + + public IEnumerator> GetEnumerator() + { + return m_Contents + .Select(v => new KeyValuePair(m_KeySelector(v), v)) + .GetEnumerator(); + } + + public bool Remove(KeyValuePair item) + { + throw new NotImplementedException(); + } + + public bool Remove(TKey key) + { + for (uint i = 0; i < m_Contents.Count; i++) + { + TKey currentKey = m_KeySelector(m_Contents[(int)i]); + + if (m_KeyComparer.Equals(currentKey, key)) + { + m_Contents.RemoveAt((int)i); + return true; + } + } + + return false; + } + + public bool TryGetValue(TKey key, out TValue value) + { + foreach (var v in m_Contents) + { + TKey currentKey = m_KeySelector(v); + if (m_KeyComparer.Equals(currentKey, key)) + { + value = v; + return true; + } + } + + value = default(TValue); + return false; + } + + IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } + + class KeyCollection : ICollection + { + readonly List m_Values; + readonly Func m_KeySelector; + readonly IEqualityComparer m_KeyComparer; + + public KeyCollection(List values, Func keySelector, IEqualityComparer keyComparer) + { + m_Values = values; + m_KeySelector = keySelector; + m_KeyComparer = keyComparer; + } + + public int Count { get { return m_Values.Count; } } + public bool IsReadOnly { get { return false; } } + + public void Add(TKey item) { throw new NotSupportedException(); } + + public void Clear() { m_Values.Clear(); } + + public bool Contains(TKey item) + { + return this.Where(k => m_KeyComparer.Equals(item, k)).Any(); + } + + public void CopyTo(TKey[] array, int arrayIndex) + { + throw new NotImplementedException(); + } + + IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } + public IEnumerator GetEnumerator() + { + return m_Values.Select(v => m_KeySelector(v)).GetEnumerator(); + } + + public bool Remove(TKey item) + { + for (uint i = 0; i < m_Values.Count; i++) + { + TKey currentKey = m_KeySelector(m_Values[(int)i]); + if (m_KeyComparer.Equals(currentKey, item)) + { + m_Values.RemoveAt((int)i); + return true; + } + } + + return false; + } + } + } +} diff --git a/src/TF2Net/Extensions/BitStreamExtensions.cs b/src/TF2Net/Extensions/BitStreamExtensions.cs new file mode 100644 index 0000000..6e3d979 --- /dev/null +++ b/src/TF2Net/Extensions/BitStreamExtensions.cs @@ -0,0 +1,25 @@ +using BitSet; +using TF2Net.Data; +using TF2Net.NetMessages; + +namespace TF2Net.Extensions +{ + public static class BitStreamExtensions + { + public static Vector ReadVector(this BitStream stream) + { + bool flagX = stream.ReadBool(); + bool flagY = stream.ReadBool(); + bool flagZ = stream.ReadBool(); + + Vector vector = new Vector(); + if (flagX) + vector.X = BitCoord.Read(stream); + if (flagY) + vector.Y = BitCoord.Read(stream); + if (flagZ) + vector.Z = BitCoord.Read(stream); + return vector; + } + } +} \ No newline at end of file diff --git a/src/TF2Net/Extensions/System.Collections.cs b/src/TF2Net/Extensions/System.Collections.cs new file mode 100644 index 0000000..714f7a5 --- /dev/null +++ b/src/TF2Net/Extensions/System.Collections.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace System.Collections +{ + [EditorBrowsable(EditorBrowsableState.Never)] + public static class TF2Net_Extensions + { + public static uint? FindNextSetBit(this BitArray source, uint startIndex = 0) + { + if (source == null) + throw new ArgumentNullException(nameof(source)); + + for (uint i = startIndex; i < source.Length; i++) + { + if (source[(int)i]) + return i; + } + + return null; + } + + public static IDictionary Clone(this IDictionary src) + { + return new Dictionary(src); + } + + public static void AddRange(this IList input, IEnumerable range) + { + if (input == null) + throw new ArgumentNullException(nameof(input)); + if (range == null) + throw new ArgumentNullException(nameof(range)); + + foreach (var x in range) + input.Add(x); + } + } +} diff --git a/src/TF2Net/Extensions/System.Linq.cs b/src/TF2Net/Extensions/System.Linq.cs new file mode 100644 index 0000000..3074227 --- /dev/null +++ b/src/TF2Net/Extensions/System.Linq.cs @@ -0,0 +1,89 @@ +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; + +namespace System.Linq +{ + [EditorBrowsable(EditorBrowsableState.Never)] + public static class TF2Net_Extensions + { + class ListWrapper : IReadOnlyList + { + readonly IList m_Source; + public ListWrapper(IList src) + { + m_Source = src; + } + + public T this[int index] { get { return m_Source[index]; } } + public int Count { get { return m_Source.Count; } } + public IEnumerator GetEnumerator() { return m_Source.GetEnumerator(); } + IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } + } + + public static IReadOnlyList AsReadOnly(this IList input) + { + if (input == null) + throw new ArgumentNullException(nameof(input)); + + return new ListWrapper(input); + } + + class DictionaryWrapper : IReadOnlyDictionary + { + readonly IDictionary m_Source; + public DictionaryWrapper(IDictionary src) + { + m_Source = src; + } + + public V this[K key] { get { return m_Source[key]; } } + public int Count { get { return m_Source.Count; } } + + public IEnumerable Keys { get { return m_Source.Keys; } } + public IEnumerable Values { get { return m_Source.Values; } } + + public bool ContainsKey(K key) + { + return m_Source.ContainsKey(key); + } + + public IEnumerator> GetEnumerator() + { + return m_Source.GetEnumerator(); + } + + public bool TryGetValue(K key, out V value) + { + return m_Source.TryGetValue(key, out value); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } + + public static IReadOnlyDictionary AsReadOnly(this IDictionary input) + { + if (input == null) + throw new ArgumentNullException(nameof(input)); + + return new DictionaryWrapper(input); + } + + public static IEnumerable Except(this IEnumerable input, T without) + { + if (input == null) + throw new ArgumentNullException(nameof(input)); + + foreach (var x in input) + { + if (x.Equals(without)) + continue; + + yield return x; + } + } + } +} diff --git a/src/TF2Net/IWorldEvents.cs b/src/TF2Net/IWorldEvents.cs new file mode 100644 index 0000000..af260f3 --- /dev/null +++ b/src/TF2Net/IWorldEvents.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using TF2Net.Data; +using TF2Net.Entities; + +namespace TF2Net +{ + public interface IWorldEvents + { + SingleEvent> GameEventsListLoaded { get; } + SingleEvent> GameEvent { get; } + + SingleEvent> ServerClassesLoaded { get; } + SingleEvent> SendTablesLoaded { get; } + + /// + /// A "Print Message" command from the server. + /// + SingleEvent> ServerTextMessage { get; } + + SingleEvent> ServerInfoLoaded { get; } + + SingleEvent> NewTick { get; } + + SingleEvent>> ServerSetConVar { get; } + SingleEvent> ServerConCommand { get; } + + SingleEvent> ViewEntityUpdated { get; } + + SingleEvent> StringTableCreated { get; } + SingleEvent> StringTableUpdated { get; } + + SingleEvent> EntityCreated { get; } + SingleEvent> EntityEnteredPVS { get; } + SingleEvent> EntityLeftPVS { get; } + SingleEvent> EntityDeleted { get; } + + SingleEvent> PlayerAdded { get; } + SingleEvent> PlayerRemoved { get; } + + SingleEvent> TempEntityCreated { get; } + } +} diff --git a/src/TF2Net/Monitors/EntityMonitor.cs b/src/TF2Net/Monitors/EntityMonitor.cs new file mode 100644 index 0000000..745f55f --- /dev/null +++ b/src/TF2Net/Monitors/EntityMonitor.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using TF2Net.Data; +using TF2Net.Entities; + +namespace TF2Net.Monitors +{ + public class EntityMonitor + { + public SingleEvent> EnteredPVS { get; } = new SingleEvent>(); + public SingleEvent> LeftPVS { get; } = new SingleEvent>(); + + public WorldState World { get; } + public string ClassName { get; } + + public EntityMonitor(WorldState ws, string classname) + { + World = ws; + ClassName = classname; + + World.Listeners.EntityEnteredPVS.Add(Entity_EnteredPVS); + World.Listeners.EntityLeftPVS.Add(Entity_LeftPVS); + } + + void Entity_EnteredPVS(Entity e) + { + if (e.Class.Classname == ClassName) + EnteredPVS.Invoke(this, e); + } + void Entity_LeftPVS(Entity e) + { + if (e.Class.Classname == ClassName) + LeftPVS.Invoke(this, e); + } + } +} diff --git a/src/TF2Net/Monitors/EntityPropertyMonitor.cs b/src/TF2Net/Monitors/EntityPropertyMonitor.cs new file mode 100644 index 0000000..71e4a7d --- /dev/null +++ b/src/TF2Net/Monitors/EntityPropertyMonitor.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using TF2Net.Data; +using TF2Net.Entities; + +namespace TF2Net.Monitors +{ + [DebuggerDisplay("{Value}")] + internal class EntityPropertyMonitor : IEntityPropertyMonitor + { + public Entity Entity { get; } + public string PropertyName { get; } + public SendProp Property { get; private set; } + + Func Decoder { get; } + + bool m_ValueChanged; + public T Value { get; private set; } + object IPropertyMonitor.Value { get { return Value; } } + + SingleEvent> IPropertyMonitor.ValueChanged { get; } = new SingleEvent>(); + SingleEvent>> IPropertyMonitor.ValueChanged { get; } = new SingleEvent>>(); + SingleEvent> IEntityPropertyMonitor.ValueChanged { get; } = new SingleEvent>(); + public SingleEvent>> ValueChanged { get; } = new SingleEvent>>(); + + public EntityPropertyMonitor(string propertyName, Entity e, Func decoder) + { + ValueChanged.Add((self) => ((IPropertyMonitor)self).ValueChanged.Invoke(self)); + ValueChanged.Add((self) => ((IPropertyMonitor)self).ValueChanged.Invoke(self)); + ValueChanged.Add((self) => ((IEntityPropertyMonitor)self).ValueChanged.Invoke(self)); + + PropertyName = propertyName; + Entity = e; + Decoder = decoder; + + Entity.EnteredPVS.Add(Entity_EnteredPVS); + Entity.LeftPVS.Add(Entity_LeftPVS); + Entity.PropertiesUpdated.Add(Entity_PropertiesUpdated); + + if (Entity.InPVS) + Entity_EnteredPVS(Entity); + } + + private void Entity_PropertiesUpdated(IPropertySet e) + { + Debug.Assert(Entity == e); + if (m_ValueChanged) + { + ValueChanged.Invoke(this); + m_ValueChanged = false; + } + } + + private void Entity_LeftPVS(Entity e) + { + Debug.Assert(Entity == e); + e.PropertyAdded.Remove(Entity_PropertyAdded); + Property = null; + } + + private void Entity_EnteredPVS(Entity e) + { + Debug.Assert(Entity == e); + e.PropertyAdded.Add(Entity_PropertyAdded); + + foreach (SendProp prop in e.Properties) + Entity_PropertyAdded(prop); + } + + private void Entity_PropertyAdded(SendProp prop) + { + if (prop.Definition.FullName == PropertyName) + { + Property = prop; + + if (prop.ValueChanged.Add(Prop_ValueChanged)) + { + // First add only + if (prop.Value != null) + Prop_ValueChanged(prop); + } + } + } + + private void Prop_ValueChanged(SendProp prop) + { + Debug.Assert(ReferenceEquals(prop.Entity, Entity)); + Debug.Assert((!Entity.InPVS && Property == null) || prop == Property); + Value = Decoder(prop.Value); + m_ValueChanged = true; + } + } +} diff --git a/src/TF2Net/Monitors/IPropertyMonitor.cs b/src/TF2Net/Monitors/IPropertyMonitor.cs new file mode 100644 index 0000000..849af64 --- /dev/null +++ b/src/TF2Net/Monitors/IPropertyMonitor.cs @@ -0,0 +1,39 @@ +using System; +using TF2Net.Data; +using TF2Net.Entities; + +namespace TF2Net.Monitors +{ + public interface IPlayerPropertyMonitor : IEntityPropertyMonitor, IPlayerPropertyMonitor + { + new SingleEvent>> ValueChanged { get; } + } + public interface IPlayerPropertyMonitor : IEntityPropertyMonitor + { + Player Player { get; } + new SingleEvent> ValueChanged { get; } + } + + public interface IEntityPropertyMonitor : IPropertyMonitor, IEntityPropertyMonitor + { + new SingleEvent>> ValueChanged { get; } + } + public interface IEntityPropertyMonitor : IPropertyMonitor + { + Entity Entity { get; } + new SingleEvent> ValueChanged { get; } + } + + public interface IPropertyMonitor : IPropertyMonitor + { + new T Value { get; } + new SingleEvent>> ValueChanged { get; } + } + public interface IPropertyMonitor + { + object Value { get; } + SendProp Property { get; } + string PropertyName { get; } + SingleEvent> ValueChanged { get; } + } +} diff --git a/src/TF2Net/Monitors/MultiPropertyMonitor.cs b/src/TF2Net/Monitors/MultiPropertyMonitor.cs new file mode 100644 index 0000000..7813fe0 --- /dev/null +++ b/src/TF2Net/Monitors/MultiPropertyMonitor.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using TF2Net.Data; + +namespace TF2Net.Monitors +{ + [DebuggerDisplay("{Value}")] + class MultiPropertyMonitor : IPropertyMonitor + { + public string PropertyName { get { return string.Join("\n", PropertyMonitors.Select(pm => pm.PropertyName)); } } + + public T Value { get; private set; } + object IPropertyMonitor.Value { get { return Value; } } + public SendProp Property { get; private set; } + + SingleEvent> IPropertyMonitor.ValueChanged { get; } = new SingleEvent>(); + public SingleEvent>> ValueChanged { get; } = new SingleEvent>>(); + + IEnumerable> PropertyMonitors { get; } + + public MultiPropertyMonitor(IEnumerable> propertyMonitors) + { + ValueChanged.Add(self => ((IPropertyMonitor)self).ValueChanged.Invoke(self)); + + PropertyMonitors = propertyMonitors; + + foreach (var prop in PropertyMonitors) + prop.ValueChanged.Add(PropValueChanged); + } + + void PropValueChanged(IPropertyMonitor propMonitor) + { + T newValue = propMonitor.Value; + if (!Value.Equals(newValue)) + { + Value = newValue; + Property = propMonitor.Property; + + if (!(Property == null || propMonitor.Property == null || propMonitor.Property.LastChangedTick >= Property.LastChangedTick)) + Debugger.Break(); + + ValueChanged.Invoke(this); + } + } + } +} diff --git a/src/TF2Net/NetMessageCoder.cs b/src/TF2Net/NetMessageCoder.cs new file mode 100644 index 0000000..f4a5d12 --- /dev/null +++ b/src/TF2Net/NetMessageCoder.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using BitSet; +using TF2Net.Data; +using TF2Net.NetMessages; + +namespace TF2Net +{ + public static class NetMessageCoder + { + public static List Decode(BitStream stream) + { + List messages = new List(); + + while (stream.Cursor < (stream.Length - SourceConstants.NETMSG_TYPE_BITS)) + { + NetMessageType type = (NetMessageType)stream.ReadByte(SourceConstants.NETMSG_TYPE_BITS); + + if (type == NetMessageType.NET_NOOP) + continue; + + INetMessage newMsg = CreateNetMessage(type); + newMsg.ReadMsg(stream); + messages.Add(newMsg); + } + + return messages; + } + + static INetMessage CreateNetMessage(NetMessageType type) + { + switch (type) + { + case NetMessageType.NET_FILE: return new NetFileMessage(); + case NetMessageType.NET_TICK: return new NetTickMessage(); + case NetMessageType.NET_STRINGCMD: return new NetStringCmdMessage(); + case NetMessageType.NET_SETCONVAR: return new NetSetConvarMessage(); + case NetMessageType.NET_SIGNONSTATE: return new NetSignonStateMessage(); + case NetMessageType.SVC_PRINT: return new NetPrintMessage(); + case NetMessageType.SVC_SERVERINFO: return new NetServerInfoMessage(); + + case NetMessageType.SVC_CLASSINFO: return new NetClassInfoMessage(); + case NetMessageType.SVC_SETPAUSE: return new NetSetPausedMessage(); + case NetMessageType.SVC_CREATESTRINGTABLE: return new NetCreateStringTableMessage(); + case NetMessageType.SVC_UPDATESTRINGTABLE: return new NetUpdateStringTableMessage(); + case NetMessageType.SVC_VOICEINIT: return new NetVoiceInitMessage(); + case NetMessageType.SVC_VOICEDATA: return new NetVoiceDataMessage(); + + case NetMessageType.SVC_SOUND: return new NetSoundMessage(); + case NetMessageType.SVC_SETVIEW: return new NetSetViewMessage(); + case NetMessageType.SVC_FIXANGLE: return new NetFixAngleMessage(); + case NetMessageType.SVC_BSPDECAL: return new NetBspDecalMessage(); + + case NetMessageType.SVC_USERMESSAGE: return new NetUsrMsgMessage(); + + case NetMessageType.SVC_ENTITYMESSAGE: return new NetEntityMessage(); + case NetMessageType.SVC_GAMEEVENT: return new NetGameEventMessage(); + case NetMessageType.SVC_PACKETENTITIES: return new NetPacketEntitiesMessage(); + case NetMessageType.SVC_TEMPENTITIES: return new NetTempEntityMessage(); + case NetMessageType.SVC_PREFETCH: return new NetPrefetchMessage(); + + case NetMessageType.SVC_GAMEEVENTLIST: return new NetGameEventListMessage(); + + default: throw new NotImplementedException(string.Format("Unimplemented {0} \"{1}\"", typeof(NetMessageType).Name, type)); + } + } + } +} diff --git a/src/TF2Net/NetMessages/INetMessage.cs b/src/TF2Net/NetMessages/INetMessage.cs new file mode 100644 index 0000000..b9ef4d5 --- /dev/null +++ b/src/TF2Net/NetMessages/INetMessage.cs @@ -0,0 +1,13 @@ +using BitSet; +using TF2Net.Data; + +namespace TF2Net.NetMessages +{ + public interface INetMessage + { + string Description { get; } + + void ReadMsg(BitStream stream); + void ApplyWorldState(WorldState ws); + } +} diff --git a/src/TF2Net/NetMessages/NetBSPDecalMessage.cs b/src/TF2Net/NetMessages/NetBSPDecalMessage.cs new file mode 100644 index 0000000..94da1ea --- /dev/null +++ b/src/TF2Net/NetMessages/NetBSPDecalMessage.cs @@ -0,0 +1,49 @@ +using System; +using System.Diagnostics; +using BitSet; +using TF2Net.Data; +using TF2Net.Extensions; + +namespace TF2Net.NetMessages +{ + [DebuggerDisplay("{Description, nq}")] + public class NetBspDecalMessage : INetMessage + { + const byte MaxDecalIndexBits = 9; + const byte MaxEdictBits = 11; + const byte SpModelIndexBits = 11; + + public string Description => string.Format("svc_bspdecal: {0} {1} {2}", Position, DecalTextureIndex, EntIndex); + public Vector Position { get; set; } + public ulong DecalTextureIndex { get; set; } + public ulong EntIndex { get; set; } + public ulong ModelIndex { get; set; } + public bool LowPrioritiy { get; set; } + + + public void ReadMsg(BitStream stream) + { + Position = stream.ReadVector(); + DecalTextureIndex = stream.ReadULong(MaxDecalIndexBits); + + bool b = stream.ReadBool(); + if (b) + { + EntIndex = stream.ReadULong(MaxEdictBits); + ModelIndex = stream.ReadULong(SpModelIndexBits); + } + else + { + EntIndex = 0; + ModelIndex = 0; + } + LowPrioritiy = stream.ReadBool(); + } + + + public void ApplyWorldState(WorldState ws) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/TF2Net/NetMessages/NetClassInfoMessage.cs b/src/TF2Net/NetMessages/NetClassInfoMessage.cs new file mode 100644 index 0000000..c4d9588 --- /dev/null +++ b/src/TF2Net/NetMessages/NetClassInfoMessage.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using BitSet; +using TF2Net.Data; + +namespace TF2Net.NetMessages +{ + [DebuggerDisplay("{Description, nq}")] + public class NetClassInfoMessage : INetMessage + { + public short ServerClassCount { get; set; } + public bool CreateOnClient { get; set; } + + public class ServerClass + { + public ushort ClassID { get; set; } + public string DataTableName { get; set; } + public string ClassName { get; set; } + } + public IList ServerClasses { get; set; } + + public string Description + { + get + { + return string.Format("svc_ClassInfo: num {0}, {1}", ServerClassCount, + CreateOnClient ? "use client classes" : "full update"); + } + } + + public void ReadMsg(BitStream stream) + { + ServerClassCount = stream.ReadShort(); + + CreateOnClient = stream.ReadBool(); + if (CreateOnClient) + return; + + byte serverClassBits = (byte)(ExtMath.Log2(ServerClassCount) + 1); + ServerClasses = new List(); + for (int i = 0; i < ServerClassCount; i++) + { + ServerClass sc = new ServerClass(); + + sc.ClassID = stream.ReadUShort(serverClassBits); + sc.ClassName = stream.ReadCString(); + sc.DataTableName = stream.ReadCString(); + + ServerClasses.Add(sc); + } + } + + public void ApplyWorldState(WorldState ws) + { + if (!CreateOnClient) + throw new NotImplementedException(); + } + } +} diff --git a/src/TF2Net/NetMessages/NetCreateStringTableMessage.cs b/src/TF2Net/NetMessages/NetCreateStringTableMessage.cs new file mode 100644 index 0000000..492fc6f --- /dev/null +++ b/src/TF2Net/NetMessages/NetCreateStringTableMessage.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using BitSet; +using Snappy; +using TF2Net.Data; +using TF2Net.NetMessages.Shared; + +namespace TF2Net.NetMessages +{ + [DebuggerDisplay("{Description, nq}")] + public class NetCreateStringTableMessage : INetMessage + { + public BitStream Data { get; set; } + + public string TableName { get; set; } + + public ushort Entries { get; set; } + public ushort MaxEntries { get; set; } + + public ushort? UserDataSize { get; set; } + public byte? UserDataSizeBits { get; set; } + + public string Description + { + get + { + return string.Format("svc_CreateStringTable: table {0}, entries {1}, bytes {2} userdatasize {3} userdatabits {4}", + TableName, MaxEntries, BitInfo.BitsToBytes(Data.Length), UserDataSize, UserDataSizeBits); + } + } + + public void ReadMsg(BitStream stream) + { + //bool isFilenames; + if (stream.ReadChar() == ':') + { + //isFilenames = true; + } + else + { + stream.Seek(-8, System.IO.SeekOrigin.Current); + //isFilenames = false; + } + + TableName = stream.ReadCString(); + + MaxEntries = stream.ReadUShort(); + int encodeBits = ExtMath.Log2(MaxEntries); + Entries = stream.ReadUShort((byte)(encodeBits + 1)); + + ulong bitCount = stream.ReadVarUInt(); + + // userdatafixedsize + if (stream.ReadBool()) + { + UserDataSize = stream.ReadUShort(12); + UserDataSizeBits = stream.ReadByte(4); + } + + bool isCompressedData = stream.ReadBool(); + + Data = stream.Subsection(stream.Cursor, stream.Cursor + bitCount); + stream.Seek(bitCount, System.IO.SeekOrigin.Current); + + if (isCompressedData) + { + uint decompressedNumBytes = Data.ReadUInt(); + uint compressedNumBytes = Data.ReadUInt(); + + byte[] compressedData = Data.ReadBytes(compressedNumBytes); + + char[] magic = Encoding.ASCII.GetChars(compressedData, 0, 4); + if ( + magic[0] != 'S' || + magic[1] != 'N' || + magic[2] != 'A' || + magic[3] != 'P') + { + throw new FormatException("Unknown format for compressed stringtable"); + } + + int snappyDecompressedNumBytes = SnappyCodec.GetUncompressedLength(compressedData, 4, compressedData.Length - 4); + if (snappyDecompressedNumBytes != decompressedNumBytes) + throw new FormatException("Mismatching decompressed data lengths"); + + byte[] decompressedData = new byte[snappyDecompressedNumBytes]; + if (SnappyCodec.Uncompress(compressedData, 4, compressedData.Length - 4, decompressedData, 0) != decompressedNumBytes) + throw new FormatException("Snappy didn't decode all the bytes"); + + Data = new BitStream(decompressedData); + } + } + + public void ApplyWorldState(WorldState ws) + { + Data.Cursor = 0; + StringTable table = new StringTable(ws, TableName, MaxEntries, UserDataSize, UserDataSizeBits); + StringTableParser.ParseUpdate(Data, table, Entries); + + StringTable foundTable = ws.StringTables.SingleOrDefault(t => t.TableName == table.TableName); + + if (foundTable != null) + throw new InvalidOperationException("Attempted to create a stringtable that already exists!"); + + ws.StringTables.Add(table); + ws.Listeners.StringTableCreated.Invoke(table); + } + } +} diff --git a/src/TF2Net/NetMessages/NetEntityMessage.cs b/src/TF2Net/NetMessages/NetEntityMessage.cs new file mode 100644 index 0000000..ff04ce0 --- /dev/null +++ b/src/TF2Net/NetMessages/NetEntityMessage.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using BitSet; +using TF2Net.Data; +using TF2Net.Entities; + +namespace TF2Net.NetMessages +{ + [DebuggerDisplay("{Description, nq}")] + public class NetEntityMessage : INetMessage + { + const int DATA_LENGTH_BITS = 11; + + public uint EntityIndex { get; set; } + public uint ClassID { get; set; } + + public BitStream Data { get; set; } + + public string Description + { + get + { + return string.Format("svc_EntityMessage: entity {0}, class {1}, bytes {2}", + EntityIndex, ClassID, BitInfo.BitsToBytes(Data.Length)); + } + } + public void WriteMsg(byte[] buffer, ref ulong bitOffset) + { + throw new NotImplementedException(); + } + + public void ReadMsg(BitStream stream) + { + EntityIndex = stream.ReadUInt(SourceConstants.MAX_EDICT_BITS); + ClassID = stream.ReadUInt(SourceConstants.MAX_SERVER_CLASS_BITS); + + ulong bitCount = stream.ReadULong(DATA_LENGTH_BITS); + Data = stream.Subsection(stream.Cursor, stream.Cursor + bitCount); + stream.Seek(bitCount, System.IO.SeekOrigin.Current); + } + + public void ApplyWorldState(WorldState ws) + { + Entity target = ws.Entities[EntityIndex]; + Console.WriteLine("hi"); + } + } +} diff --git a/src/TF2Net/NetMessages/NetFileMessage.cs b/src/TF2Net/NetMessages/NetFileMessage.cs new file mode 100644 index 0000000..bb4827a --- /dev/null +++ b/src/TF2Net/NetMessages/NetFileMessage.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using BitSet; +using TF2Net.Data; + +namespace TF2Net.NetMessages +{ + [DebuggerDisplay("{Description, nq}")] + public class NetFileMessage : INetMessage + { + public uint TransferID { get; set; } + public string Filename { get; set; } + + public enum FileStatus + { + Denied = 0, + Requested = 1, + } + + public FileStatus Status { get; set; } + + public string Description + { + get + { + return string.Format("net_File: {0} {1}", Filename, Status); + } + } + public void ReadMsg(BitStream stream) + { + TransferID = stream.ReadUInt(); + Filename = stream.ReadCString(); + + Status = (FileStatus)stream.ReadByte(1); + } + + public void ApplyWorldState(WorldState ws) + { + } + } +} diff --git a/src/TF2Net/NetMessages/NetFixAngleMessage.cs b/src/TF2Net/NetMessages/NetFixAngleMessage.cs new file mode 100644 index 0000000..29577d8 --- /dev/null +++ b/src/TF2Net/NetMessages/NetFixAngleMessage.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using BitSet; +using TF2Net.Data; + +namespace TF2Net.NetMessages +{ + [DebuggerDisplay("{Description, nq}")] + public class NetFixAngleMessage : INetMessage + { + const int ANGLE_BITS = 16; + + public bool Relative { get; set; } + public double[] Angle { get; set; } + + public string Description + { + get + { + return string.Format("svc_FixAngle: {0} {1:N1} {2:N1} {3:N1}", + Relative ? "relative" : "absolute", + Angle[0], Angle[1], Angle[2]); + } + } + + public void ReadMsg(BitStream stream) + { + Relative = stream.ReadBool(); + + Angle = new double[3]; + Angle[0] = BitAngle.Read(stream, ANGLE_BITS); + Angle[1] = BitAngle.Read(stream, ANGLE_BITS); + Angle[2] = BitAngle.Read(stream, ANGLE_BITS); + } + + public void ApplyWorldState(WorldState ws) + { + } + } +} diff --git a/src/TF2Net/NetMessages/NetGameEventListMessage.cs b/src/TF2Net/NetMessages/NetGameEventListMessage.cs new file mode 100644 index 0000000..4cb2712 --- /dev/null +++ b/src/TF2Net/NetMessages/NetGameEventListMessage.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using BitSet; +using TF2Net.Data; + +namespace TF2Net.NetMessages +{ + [DebuggerDisplay("{Description, nq}")] + public class NetGameEventListMessage : INetMessage + { + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + ulong BitCount { get; set; } + //public byte[] Data { get; set; } + + public IList Events { get; set; } + + public string Description + { + get + { + return string.Format("svc_GameEventList: number {0}, bytes {1}", + Events.Count, BitInfo.BitsToBytes(BitCount)); + } + } + + public void ReadMsg(BitStream stream) + { + ushort eventsCount = stream.ReadUShort(SourceConstants.MAX_EVENT_BITS); + + BitCount = stream.ReadULong(20); + + Events = new List(); + for (int i = 0; i < eventsCount; i++) + { + GameEventDeclaration e = new GameEventDeclaration(); + e.ID = stream.ReadInt(SourceConstants.MAX_EVENT_BITS); + e.Name = stream.ReadCString(); + + GameEventDataType type; + + e.Values = new Dictionary(); + while ((type = (GameEventDataType)stream.ReadUShort(3)) != GameEventDataType.Local) + { + string name = stream.ReadCString(); + e.Values.Add(name, type); + } + + Events.Add(e); + } + } + + public void ApplyWorldState(WorldState ws) + { + ws.EventDeclarations = Events; + } + } +} diff --git a/src/TF2Net/NetMessages/NetGameEventMessage.cs b/src/TF2Net/NetMessages/NetGameEventMessage.cs new file mode 100644 index 0000000..b20c353 --- /dev/null +++ b/src/TF2Net/NetMessages/NetGameEventMessage.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using BitSet; +using TF2Net.Data; + +namespace TF2Net.NetMessages +{ + [DebuggerDisplay("{Description, nq}")] + public class NetGameEventMessage : INetMessage + { + const int EVENT_LENGTH_BITS = 11; + + public BitStream Data { get; set; } + + public string Description + { + get + { + return string.Format("svc_GameEvent: bytes {0}", BitInfo.BitsToBytes(Data.Length)); + } + } + + public void ReadMsg(BitStream stream) + { + ulong bitCount = stream.ReadULong(EVENT_LENGTH_BITS); + Data = stream.Subsection(stream.Cursor, stream.Cursor + bitCount); + stream.Seek(bitCount, SeekOrigin.Current); + } + + public void ApplyWorldState(WorldState ws) + { + GameEvent retVal = new GameEvent(); + + int eventID = Data.ReadInt(SourceConstants.MAX_EVENT_BITS); + + retVal.Declaration = ws.EventDeclarations.Single(g => g.ID == eventID); + + retVal.Values = new Dictionary(); + foreach (var value in retVal.Declaration.Values) + { + switch (value.Value) + { + case GameEventDataType.Local: break; + case GameEventDataType.Bool: retVal.Values.Add(value.Key, Data.ReadBool()); break; + case GameEventDataType.Byte: retVal.Values.Add(value.Key, Data.ReadByte()); break; + case GameEventDataType.Float: retVal.Values.Add(value.Key, Data.ReadSingle()); break; + case GameEventDataType.Long: retVal.Values.Add(value.Key, Data.ReadInt()); break; + case GameEventDataType.Short: retVal.Values.Add(value.Key, Data.ReadShort()); break; + case GameEventDataType.String: retVal.Values.Add(value.Key, Data.ReadCString()); break; + + default: + throw new FormatException("Invalid GameEvent type"); + } + } + + ws.Listeners.GameEvent.Invoke(ws, retVal); + } + } +} diff --git a/src/TF2Net/NetMessages/NetMessageType.cs b/src/TF2Net/NetMessages/NetMessageType.cs new file mode 100644 index 0000000..f456b08 --- /dev/null +++ b/src/TF2Net/NetMessages/NetMessageType.cs @@ -0,0 +1,38 @@ +namespace TF2Net.NetMessages +{ + enum NetMessageType : byte + { + NET_NOOP = 0, + NET_DISCONNECT = 1, + NET_FILE = 2, + NET_TICK = 3, + NET_STRINGCMD = 4, + NET_SETCONVAR = 5, + NET_SIGNONSTATE = 6, + SVC_PRINT = 7, + SVC_SERVERINFO = 8, + SVC_SENDTABLE = 9, + SVC_CLASSINFO = 10, + SVC_SETPAUSE = 11, + SVC_CREATESTRINGTABLE = 12, + SVC_UPDATESTRINGTABLE = 13, + SVC_VOICEINIT = 14, + SVC_VOICEDATA = 15, + // 16 + SVC_SOUND = 17, + SVC_SETVIEW = 18, + SVC_FIXANGLE = 19, + SVC_CROSSHAIRANGLE = 20, + SVC_BSPDECAL = 21, + // 22 + SVC_USERMESSAGE = 23, + SVC_ENTITYMESSAGE = 24, + SVC_GAMEEVENT = 25, + SVC_PACKETENTITIES = 26, + SVC_TEMPENTITIES = 27, + SVC_PREFETCH = 28, + SVC_MENU = 29, + SVC_GAMEEVENTLIST = 30, + SVC_GETCVARVALUE = 31, + } +} diff --git a/src/TF2Net/NetMessages/NetPacketEntitiesMessage.cs b/src/TF2Net/NetMessages/NetPacketEntitiesMessage.cs new file mode 100644 index 0000000..95fd8c9 --- /dev/null +++ b/src/TF2Net/NetMessages/NetPacketEntitiesMessage.cs @@ -0,0 +1,250 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using BitSet; +using TF2Net.Data; +using TF2Net.Entities; +using TF2Net.NetMessages.Shared; + +namespace TF2Net.NetMessages +{ + [DebuggerDisplay("{Description, nq}")] + public class NetPacketEntitiesMessage : INetMessage + { + const int DELTA_INDEX_BITS = 32; + const int DELTA_SIZE_BITS = 20; + + public uint MaxEntries { get; set; } + public uint UpdatedEntries { get; set; } + public bool IsDelta { get; set; } + public bool UpdateBaseline { get; set; } + public BaselineIndex? Baseline { get; set; } + public int? DeltaFrom { get; set; } + + public BitStream Data { get; set; } + + public string Description + { + get + { + return string.Format("svc_PacketEntities: delta {0}, max {1}, changed {2},{3} bytes {4}", + DeltaFrom, MaxEntries, UpdatedEntries, + UpdateBaseline ? " BL update," : "", + BitInfo.BitsToBytes(Data.Length)); + } + } + + public void ReadMsg(BitStream stream) + { + MaxEntries = stream.ReadUInt(SourceConstants.MAX_EDICT_BITS); + + IsDelta = stream.ReadBool(); + if (IsDelta) + DeltaFrom = stream.ReadInt(DELTA_INDEX_BITS); + + Baseline = (BaselineIndex)stream.ReadByte(1); + + UpdatedEntries = stream.ReadUInt(SourceConstants.MAX_EDICT_BITS); + + ulong bitCount = stream.ReadULong(DELTA_SIZE_BITS); + + UpdateBaseline = stream.ReadBool(); + + Data = stream.Subsection(stream.Cursor, stream.Cursor + bitCount); + stream.Seek(bitCount, SeekOrigin.Current); + } + + static uint ReadUBitInt(BitStream stream) + { + uint ret = stream.ReadUInt(6); + switch (ret & (16 | 32)) + { + case 16: + ret = (ret & 15) | (stream.ReadUInt(4) << 4); + break; + case 32: + ret = (ret & 15) | (stream.ReadUInt(8) << 4); + break; + case 48: + ret = (ret & 15) | (stream.ReadUInt(32 - 4) << 4); + break; + } + return ret; + } + + public void ApplyWorldState(WorldState ws) + { + if (ws.SignonState.State == ConnectionState.Spawn) + { + if (!IsDelta) + { + // We are done with signon sequence. + ws.SignonState.State = ConnectionState.Full; + } + else + throw new InvalidOperationException("eceived delta packet entities while spawing!"); + } + + //ClientFrame newFrame = new ClientFrame(ws.Tick); + //ws.Frames.Add(newFrame); + //ClientFrame oldFrame = null; + if (IsDelta) + { + if (ws.Tick == (ulong)DeltaFrom.Value) + throw new InvalidDataException("Update self-referencing"); + + //oldFrame = ws.Frames.Single(f => f.ServerTick == (ulong)DeltaFrom.Value); + } + + if (UpdateBaseline) + { + if (Baseline.Value == BaselineIndex.Baseline0) + { + ws.InstanceBaselines[(int)BaselineIndex.Baseline1] = ws.InstanceBaselines[(int)BaselineIndex.Baseline0]; + ws.InstanceBaselines[(int)BaselineIndex.Baseline0] = new IList[SourceConstants.MAX_EDICTS]; + } + else if (Baseline.Value == BaselineIndex.Baseline1) + { + ws.InstanceBaselines[(int)BaselineIndex.Baseline0] = ws.InstanceBaselines[(int)BaselineIndex.Baseline1]; + ws.InstanceBaselines[(int)BaselineIndex.Baseline1] = new IList[SourceConstants.MAX_EDICTS]; + } + else + throw new ArgumentOutOfRangeException(nameof(Baseline)); + } + + Data.Seek(0, SeekOrigin.Begin); + + int newEntity = -1; + for (int i = 0; i < UpdatedEntries; i++) + { + newEntity += 1 + (int)EntityCoder.ReadUBitVar(Data); + + // Leave PVS flag + if (!Data.ReadBool()) + { + // Enter PVS flag + if (Data.ReadBool()) + { + Entity e = ReadEnterPVS(ws, Data, (uint)newEntity); + + EntityCoder.ApplyEntityUpdate(e, Data); + + if (ws.Entities[e.Index] != null && !ReferenceEquals(e, ws.Entities[e.Index])) + ws.Entities[e.Index].Dispose(); + + ws.Entities[e.Index] = e; + + if (UpdateBaseline) + ws.InstanceBaselines[Baseline.Value == BaselineIndex.Baseline0 ? 1 : 0][e.Index] = new List(e.Properties.Select(sp => sp.Clone())); + + e.InPVS = true; + } + else + { + // Preserve/update + Entity e = ws.Entities[(uint)newEntity];// ws.Entities.Single(ent => ent.Index == newEntity); + EntityCoder.ApplyEntityUpdate(e, Data); + } + } + else + { + bool shouldDelete = Data.ReadBool(); + + Entity e = ws.Entities[newEntity]; + if (e != null) + e.InPVS = false; + + ReadLeavePVS(ws, newEntity, shouldDelete); + } + } + + if (IsDelta) + { + // Read explicit deletions + while (Data.ReadBool()) + { + uint ent = Data.ReadUInt(SourceConstants.MAX_EDICT_BITS); + + //Debug.Assert(ws.Entities[ent] != null); + if (ws.Entities[ent] != null) + ws.Entities[ent].Dispose(); + + ws.Entities[ent] = null; + } + } + + //Console.WriteLine("Parsed {0}", Description); + } + + class SendPropDefinitionComparer : IEqualityComparer + { + public static SendPropDefinitionComparer Instance { get; } = new SendPropDefinitionComparer(); + private SendPropDefinitionComparer() { } + + public bool Equals(SendProp x, SendProp y) + { + return x.Definition.Equals(y.Definition); + } + + public int GetHashCode(SendProp obj) + { + return obj.Definition.GetHashCode(); + } + } + + Entity ReadEnterPVS(WorldState ws, BitStream stream, uint entityIndex) + { + ServerClass serverClass = ws.ServerClasses[(int)stream.ReadUInt(ws.ClassBits)]; + SendTable networkTable = ws.SendTables.Single(st => st.NetTableName == serverClass.DatatableName); + uint serialNumber = stream.ReadUInt(SourceConstants.NUM_NETWORKED_EHANDLE_SERIAL_NUMBER_BITS); + + Entity e; + { + Entity existing = ws.Entities[entityIndex]; + e = (existing == null || existing.SerialNumber != serialNumber) ? + new Entity(ws, serverClass, networkTable, entityIndex, serialNumber) : + existing; + } + + var decodedBaseline = ws.InstanceBaselines[(int)Baseline.Value][entityIndex]; + if (decodedBaseline != null) + { + var propertiesToAdd = + decodedBaseline + .Except(e.Properties, SendPropDefinitionComparer.Instance) + .Select(sp => sp.Clone(e)); + + foreach (var p2a in propertiesToAdd) + e.AddProperty(p2a); + } + else + { + BitStream baseline = ws.StaticBaselines.SingleOrDefault(bl => bl.Key == e.Class).Value; + if (baseline != null) + { + baseline.Cursor = 0; + EntityCoder.ApplyEntityUpdate(e, baseline); + Debug.Assert((baseline.Length - baseline.Cursor) < 8); + } + } + + return e; + } + + void ReadLeavePVS(WorldState ws, int newEntity, bool delete) + { + if (delete) + { + //Debug.Assert(ws.Entities[newEntity] != null); + + if (ws.Entities[newEntity] != null) + ws.Entities[newEntity].Dispose(); + + ws.Entities[newEntity] = null; + } + } + } +} diff --git a/src/TF2Net/NetMessages/NetPrefetchMessage.cs b/src/TF2Net/NetMessages/NetPrefetchMessage.cs new file mode 100644 index 0000000..4bc2c61 --- /dev/null +++ b/src/TF2Net/NetMessages/NetPrefetchMessage.cs @@ -0,0 +1,38 @@ +using System; +using System.Diagnostics; +using BitSet; +using TF2Net.Data; + +namespace TF2Net.NetMessages +{ + [DebuggerDisplay("{Description, nq}")] + public class NetPrefetchMessage : INetMessage + { + public enum PrefetchType + { + Sound = 0, + } + + public PrefetchType Type { get; set; } + + public int SoundIndex { get; set; } + + public string Description + { + get + { + return string.Format("svc_Prefetch: type {0} index {1}", Type, SoundIndex); + } + } + + public void ReadMsg(BitStream stream) + { + Type = PrefetchType.Sound; + SoundIndex = stream.ReadInt(SourceConstants.MAX_SOUND_INDEX_BITS); + } + + public void ApplyWorldState(WorldState ws) + { + } + } +} diff --git a/src/TF2Net/NetMessages/NetPrintMessage.cs b/src/TF2Net/NetMessages/NetPrintMessage.cs new file mode 100644 index 0000000..4a8bee4 --- /dev/null +++ b/src/TF2Net/NetMessages/NetPrintMessage.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using BitSet; +using TF2Net.Data; + +namespace TF2Net.NetMessages +{ + [DebuggerDisplay("{Description, nq}")] + public class NetPrintMessage : INetMessage + { + public string Message { get; set; } + + public string Description + { + get + { + return string.Format("svc_Print: \"{0}\"", Message); + } + } + + public void ReadMsg(BitStream stream) + { + Message = stream.ReadCString(); + } + + public void ApplyWorldState(WorldState ws) + { + ws.Listeners.ServerTextMessage.Invoke(ws, Message); + } + } +} diff --git a/src/TF2Net/NetMessages/NetServerInfoMessage.cs b/src/TF2Net/NetMessages/NetServerInfoMessage.cs new file mode 100644 index 0000000..55c2128 --- /dev/null +++ b/src/TF2Net/NetMessages/NetServerInfoMessage.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using BitSet; +using TF2Net.Data; + +namespace TF2Net.NetMessages +{ + [DebuggerDisplay("{Description, nq}")] + public class NetServerInfoMessage : INetMessage + { + public ServerInfo Info { get; set; } + + public string Description + { + get + { + return string.Format("svc_ServerInfo: game \"{0}\", map \"{1}\", max {2}", + Info.GameDirectory, Info.MapName, Info.MaxClients); + } + } + + public void ReadMsg(BitStream stream) + { + Info = new ServerInfo(); + + Info.Protocol = stream.ReadShort(); + Info.ServerCount = stream.ReadInt(); + Info.IsHLTV = stream.ReadBool(); + Info.IsDedicated = stream.ReadBool(); + Info.ClientCRC = stream.ReadUInt(); + Info.MaxClasses = stream.ReadUShort(); + + // Unknown + stream.Seek(16 * 8, System.IO.SeekOrigin.Current); + + Info.PlayerSlot = stream.ReadByte(); + Info.MaxClients = stream.ReadByte(); + Info.TickInterval = stream.ReadSingle(); + + switch (stream.ReadChar()) + { + case 'l': + case 'L': + Info.OS = ServerInfo.OperatingSystem.Linux; + break; + + case 'w': + case 'W': + Info.OS = ServerInfo.OperatingSystem.Windows; + break; + + default: + Info.OS = ServerInfo.OperatingSystem.Unknown; + break; + } + + Info.GameDirectory = stream.ReadCString(); + Info.MapName = stream.ReadCString(); + Info.SkyName = stream.ReadCString(); + Info.Hostname = stream.ReadCString(); + + // Unknown + stream.Seek(1, System.IO.SeekOrigin.Current); + } + + public void ApplyWorldState(WorldState ws) + { + ws.ServerInfo = Info; + ws.Listeners.ServerInfoLoaded.Invoke(ws); + } + } +} diff --git a/src/TF2Net/NetMessages/NetSetConvarMessage.cs b/src/TF2Net/NetMessages/NetSetConvarMessage.cs new file mode 100644 index 0000000..7129f6a --- /dev/null +++ b/src/TF2Net/NetMessages/NetSetConvarMessage.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using BitSet; +using TF2Net.Data; + +namespace TF2Net.NetMessages +{ + [DebuggerDisplay("{Description, nq}")] + public class NetSetConvarMessage : INetMessage + { + public IList> Cvars { get; set; } + + public string Description + { + get + { + return string.Format("net_SetConVar: {0} cvars, \"{1}\"=\"{2}\"", + Cvars.Count, Cvars[0].Key, Cvars[0].Value); + } + } + + public ulong Size + { + get + { + throw new NotImplementedException(); + } + } + + public void WriteMsg(byte[] buffer, ref ulong bitOffset) + { + throw new NotImplementedException(); + } + + public void ReadMsg(BitStream stream) + { + byte count = stream.ReadByte(); + + Cvars = new List>(count); + for (int i = 0; i < count; i++) + { + string name = stream.ReadCString(); + string value = stream.ReadCString(); + Cvars.Add(new KeyValuePair(name, value)); + } + } + + public void ApplyWorldState(WorldState ws) + { + foreach (var cvar in Cvars) + { + ws.ConVars[cvar.Key] = cvar.Value; + ws.Listeners.ServerSetConVar.Invoke(ws, cvar); + } + } + } +} diff --git a/src/TF2Net/NetMessages/NetSetPausedMessage.cs b/src/TF2Net/NetMessages/NetSetPausedMessage.cs new file mode 100644 index 0000000..ea75a70 --- /dev/null +++ b/src/TF2Net/NetMessages/NetSetPausedMessage.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using BitSet; +using TF2Net.Data; + +namespace TF2Net.NetMessages +{ + [DebuggerDisplay("{Description, nq}")] + public class NetSetPausedMessage : INetMessage + { + public bool Paused { get; set; } + + public string Description + { + get + { + return string.Format("svc_SetPause: {0}", Paused ? "Paused" : "Unpaused"); + } + } + + public void ReadMsg(BitStream stream) + { + Paused = stream.ReadBool(); + } + + public void ApplyWorldState(WorldState ws) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/TF2Net/NetMessages/NetSetViewMessage.cs b/src/TF2Net/NetMessages/NetSetViewMessage.cs new file mode 100644 index 0000000..ba8e675 --- /dev/null +++ b/src/TF2Net/NetMessages/NetSetViewMessage.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using BitSet; +using TF2Net.Data; + +namespace TF2Net.NetMessages +{ + [DebuggerDisplay("{Description, nq}")] + public class NetSetViewMessage : INetMessage + { + public ushort EntityIndex { get; set; } + + public string Description + { + get + { + return string.Format("svc_SetView: view entity {0}", EntityIndex); + } + } + + public void ReadMsg(BitStream stream) + { + EntityIndex = stream.ReadUShort(SourceConstants.MAX_EDICT_BITS); + } + + public void ApplyWorldState(WorldState ws) + { + ws.ViewEntity = EntityIndex; + } + } +} diff --git a/src/TF2Net/NetMessages/NetSignonStateMessage.cs b/src/TF2Net/NetMessages/NetSignonStateMessage.cs new file mode 100644 index 0000000..0e444fa --- /dev/null +++ b/src/TF2Net/NetMessages/NetSignonStateMessage.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using BitSet; +using TF2Net.Data; + +namespace TF2Net.NetMessages +{ + [DebuggerDisplay("{Description, nq}")] + public class NetSignonStateMessage : INetMessage + { + public SignonState State { get; set; } + + public string Description + { + get + { + return string.Format("net_SignonState: state {0}, count {1}", State.State, State.SpawnCount); + } + } + + public void ReadMsg(BitStream stream) + { + State = new SignonState(); + + State.State = (ConnectionState)stream.ReadByte(); + State.SpawnCount = stream.ReadInt(); + } + + public void ApplyWorldState(WorldState ws) + { + ws.SignonState = State; + } + } +} diff --git a/src/TF2Net/NetMessages/NetSoundMessage.cs b/src/TF2Net/NetMessages/NetSoundMessage.cs new file mode 100644 index 0000000..2fbd9ad --- /dev/null +++ b/src/TF2Net/NetMessages/NetSoundMessage.cs @@ -0,0 +1,54 @@ +using System; +using System.Diagnostics; +using BitSet; +using TF2Net.Data; + +namespace TF2Net.NetMessages +{ + [DebuggerDisplay("{Description, nq}")] + public class NetSoundMessage : INetMessage + { + const int SOUND_COUNT_BITS = 8; + const int RELIABLE_SIZE_BITS = 8; + const int UNRELIABLE_SIZE_BITS = 16; + + public bool Reliable { get; set; } + public int SoundCount { get; set; } + + public BitStream Data { get; set; } + + public string Description + { + get + { + return string.Format("svc_Sounds: number {0},{1} bytes {2}", + SoundCount, Reliable ? " reliable," : "", BitInfo.BitsToBytes(Data.Length)); + } + } + + public void ReadMsg(BitStream stream) + { + Reliable = stream.ReadBool(); + + ulong bitCount; + if (Reliable) + { + SoundCount = 1; + bitCount = stream.ReadULong(RELIABLE_SIZE_BITS); + } + else + { + SoundCount = stream.ReadInt(SOUND_COUNT_BITS); + bitCount = stream.ReadULong(UNRELIABLE_SIZE_BITS); + } + + Data = stream.Subsection(stream.Cursor, stream.Cursor + bitCount); + stream.Seek(bitCount, System.IO.SeekOrigin.Current); + } + + public void ApplyWorldState(WorldState ws) + { + //throw new NotImplementedException(); + } + } +} diff --git a/src/TF2Net/NetMessages/NetStringCmdMessage.cs b/src/TF2Net/NetMessages/NetStringCmdMessage.cs new file mode 100644 index 0000000..d50ac6a --- /dev/null +++ b/src/TF2Net/NetMessages/NetStringCmdMessage.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using BitSet; +using TF2Net.Data; + +namespace TF2Net.NetMessages +{ + [DebuggerDisplay("{Description, nq}")] + public class NetStringCmdMessage : INetMessage + { + public string Command { get; set; } + + public string Description + { + get + { + return string.Format("net_StringCmd: \"{0}\"", Command); + } + } + + public void ReadMsg(BitStream stream) + { + Command = stream.ReadCString(); + } + + public void ApplyWorldState(WorldState ws) + { + ws.Listeners.ServerConCommand.Invoke(ws, Command); + } + } +} diff --git a/src/TF2Net/NetMessages/NetTempEntityMessage.cs b/src/TF2Net/NetMessages/NetTempEntityMessage.cs new file mode 100644 index 0000000..e209985 --- /dev/null +++ b/src/TF2Net/NetMessages/NetTempEntityMessage.cs @@ -0,0 +1,108 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using BitSet; +using TF2Net.Data; +using TF2Net.Entities; + +namespace TF2Net.NetMessages +{ + [DebuggerDisplay("{Description, nq}")] + public class NetTempEntityMessage : INetMessage + { + public int EntryCount { get; set; } + + public BitStream Data { get; set; } + + public string Description + { + get + { + return string.Format("svc_TempEntities: number {0}, bytes {1}", EntryCount, BitInfo.BitsToBytes(Data.Length)); + } + } + + public void ReadMsg(BitStream stream) + { + EntryCount = stream.ReadInt(SourceConstants.EVENT_INDEX_BITS); + + ulong bitCount = stream.ReadVarUInt(); + + Data = stream.Subsection(stream.Cursor, stream.Cursor + bitCount); + stream.Seek(bitCount, System.IO.SeekOrigin.Current); + } + + public void ApplyWorldState(WorldState ws) + { + List tempents = new List(); + { + BitStream local = Data.Clone(); + local.Cursor = 0; + + TempEntity e = null; + for (int i = 0; i < EntryCount; i++) + { + double delay = 0; + if (local.ReadBool()) + delay = local.ReadInt(8) / 100.0; + + if (local.ReadBool()) + { + uint classID = local.ReadUInt(ws.ClassBits); + + ServerClass serverClass = ws.ServerClasses[(int)classID - 1]; + SendTable sendTable = ws.SendTables.Single(st => st.NetTableName == serverClass.DatatableName); + var flattened = sendTable.FlattenedProps; + + e = new TempEntity(ws, serverClass, sendTable); + EntityCoder.ApplyEntityUpdate(e, local); + tempents.Add(e); + } + else + { + Debug.Assert(e != null); + EntityCoder.ApplyEntityUpdate(e, local); + } + } + } + + foreach (IBaseEntity te in tempents) + { + ws.Listeners.TempEntityCreated.Invoke(te); + } + } + + [DebuggerDisplay("{Class,nq}")] + class TempEntity : IEntity + { + readonly List m_Properties = new List(); + public IReadOnlyList Properties { get { return m_Properties; } } + + public SingleEvent> PropertiesUpdated { get; } = new SingleEvent>(); + public SingleEvent> PropertyAdded { get; } = new SingleEvent>(); + + public WorldState World { get; } + public ServerClass Class { get; } + public SendTable NetworkTable { get; } + + public TempEntity(WorldState ws, ServerClass sClass, SendTable table) + { + World = ws; + Class = sClass; + NetworkTable = table; + } + + public void AddProperty(SendProp newProp) + { + Debug.Assert(!m_Properties.Any(p => ReferenceEquals(p.Definition, newProp.Definition))); + Debug.Assert(ReferenceEquals(newProp.Entity, this)); + + m_Properties.Add(newProp); + PropertyAdded.Invoke(newProp); + } + } + } +} diff --git a/src/TF2Net/NetMessages/NetTickMessage.cs b/src/TF2Net/NetMessages/NetTickMessage.cs new file mode 100644 index 0000000..3cf8355 --- /dev/null +++ b/src/TF2Net/NetMessages/NetTickMessage.cs @@ -0,0 +1,44 @@ +using System; +using System.Diagnostics; +using BitSet; +using TF2Net.Data; + +namespace TF2Net.NetMessages +{ + [DebuggerDisplay("{Description, nq}")] + public class NetTickMessage : INetMessage + { + const int TICK_BITS = 32; + const int FLOAT_BITS = 16; + const double NET_TICK_SCALEUP = 100000.0; + + public uint Tick { get; set; } + public double HostFrameTime { get; set; } + public double HostFrameTimeStdDev { get; set; } + + public string Description + { + get + { + return string.Format("net_Tick: tick {0}", Tick); + } + } + + public void ReadMsg(BitStream stream) + { + Tick = stream.ReadUInt(TICK_BITS); + + HostFrameTime = stream.ReadUInt(FLOAT_BITS) / NET_TICK_SCALEUP; + HostFrameTimeStdDev = stream.ReadUInt(FLOAT_BITS) / NET_TICK_SCALEUP; + } + + public void ApplyWorldState(WorldState ws) + { + ws.Tick = Tick; + ws.LastFrameTime = HostFrameTime; + ws.LastFrameTimeStdDev = HostFrameTimeStdDev; + + ws.Listeners.NewTick.Invoke(ws); + } + } +} diff --git a/src/TF2Net/NetMessages/NetUpdateStringTableMessage.cs b/src/TF2Net/NetMessages/NetUpdateStringTableMessage.cs new file mode 100644 index 0000000..36f765f --- /dev/null +++ b/src/TF2Net/NetMessages/NetUpdateStringTableMessage.cs @@ -0,0 +1,54 @@ +using System; +using System.Diagnostics; +using BitSet; +using TF2Net.Data; +using TF2Net.NetMessages.Shared; + +namespace TF2Net.NetMessages +{ + [DebuggerDisplay("{Description, nq}")] + public class NetUpdateStringTableMessage : INetMessage + { + const int MAX_TABLE_BITS = 5; + const int MAX_TABLES = (1 << MAX_TABLE_BITS); + const int DATA_LENGTH_BITS = 20; + const int CHANGED_ENTRIES_BITS = 16; + + public int TableID { get; set; } + public int ChangedEntries { get; set; } + + public BitStream Data { get; set; } + + public string Description + { + get + { + return string.Format("svc_UpdateStringTable: table {0}, changed {1}, bytes {2}", + TableID, ChangedEntries, BitInfo.BitsToBytes(Data.Length)); + } + } + + public void ReadMsg(BitStream stream) + { + TableID = stream.ReadInt(MAX_TABLE_BITS); + + bool multipleChanged = stream.ReadBool(); + if (!multipleChanged) + ChangedEntries = 1; + else + ChangedEntries = stream.ReadInt(CHANGED_ENTRIES_BITS); + + ulong bitCount = stream.ReadULong(DATA_LENGTH_BITS); + Data = stream.Subsection(stream.Cursor, stream.Cursor + bitCount); + stream.Seek(bitCount, System.IO.SeekOrigin.Current); + } + + public void ApplyWorldState(WorldState ws) + { + StringTable found = ws.StringTables[TableID]; + StringTableParser.ParseUpdate(Data, found, (ushort)ChangedEntries); + + ws.Listeners.StringTableUpdated.Invoke(ws, found); + } + } +} diff --git a/src/TF2Net/NetMessages/NetUsrMsgMessage.cs b/src/TF2Net/NetMessages/NetUsrMsgMessage.cs new file mode 100644 index 0000000..3c47437 --- /dev/null +++ b/src/TF2Net/NetMessages/NetUsrMsgMessage.cs @@ -0,0 +1,42 @@ +using System; +using System.Diagnostics; +using System.Linq; +using BitSet; +using TF2Net.Data; + +namespace TF2Net.NetMessages +{ + [DebuggerDisplay("{Description, nq}")] + public class NetUsrMsgMessage : INetMessage + { + const int MAX_USER_MSG_TYPE_BITS = 8; + + public UserMessageType Type { get; set; } + + public BitStream Data { get; set; } + + public string Description + { + get + { + return string.Format("svc_UserMessage: type {0}, bytes {1}", Type, BitInfo.BitsToBytes(Data.Length)); + } + } + + public void ReadMsg(BitStream stream) + { + Type = (UserMessageType)stream.ReadInt(MAX_USER_MSG_TYPE_BITS); + Debug.Assert(Enum.GetValues(typeof(UserMessageType)).Cast().Contains(Type)); + + ulong bitCount = stream.ReadULong(SourceConstants.MAX_USER_MSG_LENGTH_BITS); + Data = stream.Subsection(stream.Cursor, stream.Cursor + bitCount); + stream.Seek(bitCount, System.IO.SeekOrigin.Current); + } + + public void ApplyWorldState(WorldState ws) + { + return; + //throw new NotImplementedException(); + } + } +} diff --git a/src/TF2Net/NetMessages/NetVoiceDataMessage.cs b/src/TF2Net/NetMessages/NetVoiceDataMessage.cs new file mode 100644 index 0000000..8f6bb17 --- /dev/null +++ b/src/TF2Net/NetMessages/NetVoiceDataMessage.cs @@ -0,0 +1,39 @@ +using System; +using System.Diagnostics; +using BitSet; +using TF2Net.Data; + +namespace TF2Net.NetMessages +{ + [DebuggerDisplay("{Description, nq}")] + public class NetVoiceDataMessage : INetMessage + { + public byte ClientIndex { get; set; } + public bool Proximity { get; set; } + + public BitStream Data { get; set; } + + public string Description + { + get + { + return string.Format("svc_VoiceData: client {0}, bytes {1}", + ClientIndex, BitInfo.BitsToBytes(Data.Length)); + } + } + + public void ReadMsg(BitStream stream) + { + ClientIndex = stream.ReadByte(); + Proximity = stream.ReadByte() != 0; + + ulong bitCount = stream.ReadULong(16); + Data = stream.Subsection(stream.Cursor, stream.Cursor + bitCount); + stream.Seek(bitCount, System.IO.SeekOrigin.Current); + } + + public void ApplyWorldState(WorldState ws) + { + } + } +} diff --git a/src/TF2Net/NetMessages/NetVoiceInitMessage.cs b/src/TF2Net/NetMessages/NetVoiceInitMessage.cs new file mode 100644 index 0000000..9dfa298 --- /dev/null +++ b/src/TF2Net/NetMessages/NetVoiceInitMessage.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using BitSet; +using TF2Net.Data; + +namespace TF2Net.NetMessages +{ + [DebuggerDisplay("{Description, nq}")] + public class NetVoiceInitMessage : INetMessage + { + public string VoiceCodec { get; set; } + public byte Quality { get; set; } + + public string Description + { + get + { + return string.Format("svc_VoiceInit: codec \"{0}\", qualitty {1}", + VoiceCodec, Quality); + } + } + + public void ReadMsg(BitStream stream) + { + VoiceCodec = stream.ReadCString(); + Quality = stream.ReadByte(); + } + + public void ApplyWorldState(WorldState ws) + { + //throw new NotImplementedException(); + } + } +} diff --git a/src/TF2Net/NetMessages/Shared/BitAngle.cs b/src/TF2Net/NetMessages/Shared/BitAngle.cs new file mode 100644 index 0000000..bd66525 --- /dev/null +++ b/src/TF2Net/NetMessages/Shared/BitAngle.cs @@ -0,0 +1,15 @@ +using BitSet; + +namespace TF2Net.NetMessages +{ + static class BitAngle + { + public static double Read(BitStream stream, byte bitCount) + { + double shift = (1 << bitCount); + + var rawValue = stream.ReadULong(bitCount); + return rawValue * (360 / shift); + } + } +} diff --git a/src/TF2Net/NetMessages/Shared/BitCoord.cs b/src/TF2Net/NetMessages/Shared/BitCoord.cs new file mode 100644 index 0000000..34f0693 --- /dev/null +++ b/src/TF2Net/NetMessages/Shared/BitCoord.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using BitSet; + +namespace TF2Net.NetMessages +{ + static class BitCoord + { + const uint COORD_INTEGER_BITS = 14; + const uint COORD_FRACTIONAL_BITS = 5; + const uint COORD_DENOMINATOR = (1 << (int)(COORD_FRACTIONAL_BITS)); + const double COORD_RESOLUTION = (1.0 / (COORD_DENOMINATOR)); + + public static double Read(byte[] source, ref ulong readBitOffset) + { + // Read the required integer and fraction flags + int intVal = BitReader.ReadUInt1(source, ref readBitOffset); + int fractVal = BitReader.ReadUInt1(source, ref readBitOffset); + + // If we got either parse them, otherwise it's a zero. + if (intVal != 0 || fractVal != 0) + { + // Read the sign bit + var signBit = BitReader.ReadUInt1(source, ref readBitOffset); + + // If there's an integer, read it in + if (intVal != 0) + { + // Adjust the integers from [0..MAX_COORD_VALUE-1] to [1..MAX_COORD_VALUE] + intVal = (int)BitReader.ReadUIntBits(source, ref readBitOffset, (byte)COORD_INTEGER_BITS); + intVal++; + } + + // If there's a fraction, read it in + if (fractVal != 0) + { + fractVal = (int)BitReader.ReadUIntBits(source, ref readBitOffset, (byte)COORD_FRACTIONAL_BITS); + } + + // Calculate the correct floating point value + double retVal = Math.Abs(intVal + ((float)fractVal * COORD_RESOLUTION)); + + // Fixup the sign if negative. + if (signBit != 0) + retVal = -retVal; + + return retVal; + } + + return 0; + } + + public static double Read(BitStream source) + { + // Read the required integer and fraction flags + bool intFlag = source.ReadBool(); + bool fractFlag = source.ReadBool(); + ulong intVal = 0; + ulong fractVal = 0; + + // If we got either parse them, otherwise it's a zero. + if (intFlag || fractFlag) + { + // Read the sign bit + var signBit = source.ReadBool(); + + // If there's an integer, read it in + if (intFlag) + { + // Adjust the integers from [0..MAX_COORD_VALUE-1] to [1..MAX_COORD_VALUE] + intVal = source.ReadULong((byte)COORD_INTEGER_BITS) + 1; + } + + // If there's a fraction, read it in + if (fractFlag) + { + fractVal = source.ReadULong((byte)COORD_FRACTIONAL_BITS); + } + + // Calculate the correct floating point value + double retVal = Math.Abs(intVal + (fractVal * COORD_RESOLUTION)); + + // Fixup the sign if negative. + if (signBit) + retVal = -retVal; + + return retVal; + } + + return 0; + } + } +} diff --git a/src/TF2Net/NetMessages/Shared/EntityCoder.cs b/src/TF2Net/NetMessages/Shared/EntityCoder.cs new file mode 100644 index 0000000..4d183fd --- /dev/null +++ b/src/TF2Net/NetMessages/Shared/EntityCoder.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using BitSet; +using TF2Net.Data; +using TF2Net.Entities; + +namespace TF2Net.NetMessages +{ + internal static class EntityCoder + { + public static void ApplyEntityUpdate(IEntity e, BitStream stream) + { + var testGuessProps = e.NetworkTable.FlattenedProps; + + bool atLeastOne = false; + int index = -1; + while ((index = ReadFieldIndex(stream, index)) != -1) + { + Debug.Assert(index < testGuessProps.Length); + Debug.Assert(index < SourceConstants.MAX_DATATABLE_PROPS); + + var prop = testGuessProps[index]; + + SendProp s = e.GetProperty(prop); + + bool wasNull = false; + if (s == null) + { + s = new SendProp(e, prop); + wasNull = true; + } + + object newValue = prop.Decode(stream); + s.Value = newValue; + atLeastOne = true; + + if (wasNull) + e.AddProperty(s); + } + + if (atLeastOne) + e.PropertiesUpdated.Invoke(e); + } + + public static int ReadFieldIndex(BitStream stream, int lastIndex) + { + if (!stream.ReadBool()) + return -1; + + var diff = ReadUBitVar(stream); + return (int)(lastIndex + diff + 1); + } + + public static uint ReadUBitVar(BitStream stream) + { + switch (stream.ReadByte(2)) + { + case 0: return stream.ReadUInt(4); + case 1: return stream.ReadUInt(8); + case 2: return stream.ReadUInt(12); + case 3: return stream.ReadUInt(32); + } + + throw new Exception("Should never get here..."); + } + } +} diff --git a/src/TF2Net/NetMessages/Shared/ExtMath.cs b/src/TF2Net/NetMessages/Shared/ExtMath.cs new file mode 100644 index 0000000..088a799 --- /dev/null +++ b/src/TF2Net/NetMessages/Shared/ExtMath.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TF2Net.NetMessages +{ + static class ExtMath + { + public static int Log2(int x) + { + int answer = 0; + while ((x >>= 1) > 0) + answer++; + return answer; + } + } +} diff --git a/src/TF2Net/NetMessages/Shared/StringTableParser.cs b/src/TF2Net/NetMessages/Shared/StringTableParser.cs new file mode 100644 index 0000000..4df7800 --- /dev/null +++ b/src/TF2Net/NetMessages/Shared/StringTableParser.cs @@ -0,0 +1,124 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using BitSet; +using TF2Net.Data; + +namespace TF2Net.NetMessages.Shared +{ + static class StringTableParser + { + const byte SUBSTRING_BITS = 5; + const byte MAX_USERDATA_BITS = 14; + + public static void ParseUpdate(BitStream stream, StringTable table, ushort entries) + { + Debug.Assert(stream.Cursor == 0); + + IList history = new List(); + + byte entryBits = (byte)ExtMath.Log2(table.MaxEntries); + int lastEntry = -1; + for (int i = 0; i < entries; i++) + { + // Did we read the entry from the BitStream or just assume it was lastEntry + 1? + //bool readEntry = false; + + int entryIndex = lastEntry + 1; + + if (!stream.ReadBool()) + { + entryIndex = (int)stream.ReadUInt(entryBits); + //readEntry = true; + } + + lastEntry = entryIndex; + + if (entryIndex < 0 || entryIndex > table.MaxEntries) + throw new FormatException("Server sent bogus string index for stringtable"); + + string value = null; + if (stream.ReadBool()) + { + bool substringcheck = stream.ReadBool(); + + if (substringcheck) + { + int index = (int)stream.ReadUInt(5); + int bytesToCopy = (int)stream.ReadUInt(SUBSTRING_BITS); + + string restOfString = stream.ReadCString(); + + var testLength = history[index].Value?.Length; + + value = history[index].Value?.Substring(0, bytesToCopy) + restOfString; + } + else + { + value = stream.ReadCString(); + } + } + + BitStream userData = null; + if (stream.ReadBool()) + { + if (table.UserDataSize.HasValue) + { + userData = stream.Subsection(stream.Cursor, stream.Cursor + table.UserDataSizeBits.Value); + stream.Seek(table.UserDataSizeBits.Value, System.IO.SeekOrigin.Current); + } + else + { + ulong nBytes = stream.ReadUInt(MAX_USERDATA_BITS); + userData = stream.Subsection(stream.Cursor, stream.Cursor + (nBytes * 8)); + stream.Seek(nBytes * 8, System.IO.SeekOrigin.Current); + } + } + + StringTableEntry existingEntry = table.Entries.SingleOrDefault(s => s.ID == entryIndex); + if (existingEntry != null) + { + existingEntry.UserData = userData; + + if (value != null && value != existingEntry.Value) + { + //throw new FormatException("Corrupted demo?"); + existingEntry.Value = value; + } + else + value = existingEntry.Value; + + existingEntry.Value = value; + } + else + { + //Debug.Assert(entryIndex == table.Entries.Count); + if (value == null) + Debug.Assert(true); + //Debug.Assert(value != null); + + //if (value == null) + // value = string.Empty; + + StringTableEntry newEntry = new StringTableEntry(table); + newEntry.ID = (ushort)entryIndex; + newEntry.UserData = userData; + newEntry.Value = value; + table.Add(newEntry); + + existingEntry = newEntry; + } + + if (history.Count > 31) + history.RemoveAt(0); + + history.Add(existingEntry); + } + + Debug.Assert((stream.Length - stream.Cursor) < 8); + } + } +} diff --git a/src/TF2Net/SingleEvent.cs b/src/TF2Net/SingleEvent.cs new file mode 100644 index 0000000..8773f02 --- /dev/null +++ b/src/TF2Net/SingleEvent.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading.Tasks; + +namespace TF2Net +{ + public class SingleEvent where T : class + { + readonly List m_Keys = new List(); + readonly ConditionalWeakTable> m_Delegates = new ConditionalWeakTable>(); + + public SingleEvent() + { + if (!typeof(T).IsSubclassOf(typeof(Delegate))) + throw new InvalidOperationException(typeof(T).Name + " is not a delegate type"); + } + + public bool Add(T input) + { + CleanKeysList(); + Delegate forceCast = (Delegate)(object)input; + + object target = forceCast.Target; + Debug.Assert(target != null); + + List delegates = m_Delegates.GetValue(target, key => + { + lock (m_Keys) + { + if (!m_Keys.Contains(target)) + m_Keys.Add(new WeakReference(target)); + } + + return new List(); + }); + + lock (delegates) + { + if (delegates.Contains(forceCast)) + return false; + + delegates.Add(forceCast); + } + + return true; + } + public bool Remove(T input) + { + Delegate forceCast = (Delegate)(object)input; + + object target = forceCast.Target; + Debug.Assert(target != null); + + List values; + m_Delegates.TryGetValue(target, out values); + + lock (values) + return values.Remove(forceCast); + } + + void CleanKeysList() + { + lock (m_Keys) + { + for (int i = 0; i < m_Keys.Count; i++) + { + if (!m_Keys[i].IsAlive) + m_Keys.RemoveAt(i--); + } + } + } + + public void Invoke(params object[] args) + { + if (m_Keys.Count > 0) + { + IEnumerable all = Enumerable.Empty(); + + lock (m_Keys) + { + IEnumerable validObjects = m_Keys.Select(k => k.Target).Where(t => t != null); + foreach (object o in validObjects) + { + List delegates; + if (m_Delegates.TryGetValue(o, out delegates)) + all = all.Concat(delegates); + } + } + + foreach (Delegate d in all) + { + var test = d.Target; + d.DynamicInvoke(args); + } + } + } + } +} diff --git a/src/TF2Net/TF2Net.csproj b/src/TF2Net/TF2Net.csproj new file mode 100644 index 0000000..504da28 --- /dev/null +++ b/src/TF2Net/TF2Net.csproj @@ -0,0 +1,158 @@ + + + + + Debug + AnyCPU + {2C2AF7CC-4368-4C03-B7E1-356D3BB89F4F} + Library + Properties + TF2Net + TF2Net + v4.5.2 + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + true + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + true + + + + ..\packages\Crc32C.NET.1.0.5.0\lib\net20\Crc32C.NET.dll + True + + + ..\packages\Snappy.NET.1.1.1.8\lib\net45\Snappy.NET.dll + True + + + + ..\packages\System.Collections.Immutable.1.2.0\lib\portable-net45+win8+wp8+wpa81\System.Collections.Immutable.dll + True + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {8f8a6380-5b10-4891-ac21-3d03494b3e44} + BitSet + + + + + + + + \ No newline at end of file diff --git a/src/TF2Net/WorldEvents.cs b/src/TF2Net/WorldEvents.cs new file mode 100644 index 0000000..bd55378 --- /dev/null +++ b/src/TF2Net/WorldEvents.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using TF2Net.Data; +using TF2Net.Entities; + +namespace TF2Net +{ + public class WorldEvents : IWorldEvents + { + public SingleEvent> EntityCreated { get; } = new SingleEvent>(); + public SingleEvent> EntityDeleted { get; } = new SingleEvent>(); + + public SingleEvent> EntityEnteredPVS { get; } = new SingleEvent>(); + public SingleEvent> EntityLeftPVS { get; } = new SingleEvent>(); + + public SingleEvent> GameEventsListLoaded { get; } = new SingleEvent>(); + public SingleEvent> GameEvent { get; } = new SingleEvent>(); + + public SingleEvent> NewTick { get; } = new SingleEvent>(); + + public SingleEvent> PlayerAdded { get; } = new SingleEvent>(); + public SingleEvent> PlayerRemoved { get; } = new SingleEvent>(); + + public SingleEvent> SendTablesLoaded { get; } = new SingleEvent>(); + + public SingleEvent> ServerClassesLoaded { get; } = new SingleEvent>(); + + public SingleEvent> ServerInfoLoaded { get; } = new SingleEvent>(); + + public SingleEvent>> ServerSetConVar { get; } = new SingleEvent>>(); + + public SingleEvent> ServerConCommand { get; } = new SingleEvent>(); + public SingleEvent> ServerTextMessage { get; } = new SingleEvent>(); + + public SingleEvent> StringTableCreated { get; } = new SingleEvent>(); + public SingleEvent> StringTableUpdated { get; } = new SingleEvent>(); + + public SingleEvent> ViewEntityUpdated { get; } = new SingleEvent>(); + + public SingleEvent> TempEntityCreated { get; } = new SingleEvent>(); + } +} diff --git a/src/TF2Net/packages.config b/src/TF2Net/packages.config new file mode 100644 index 0000000..16b657b --- /dev/null +++ b/src/TF2Net/packages.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/src.csproj b/src/src.csproj index f02677b..fae2ccd 100644 --- a/src/src.csproj +++ b/src/src.csproj @@ -5,6 +5,11 @@ net7.0 enable enable + true + + + +