diff --git a/Core/Buffer.cs b/Core/Buffer.cs
new file mode 100644
index 0000000..49a320a
--- /dev/null
+++ b/Core/Buffer.cs
@@ -0,0 +1,182 @@
+using System;
+using System.Runtime.InteropServices;
+using System.Text;
+using GTA.Math;
+
+namespace RageCoop.Core
+{
+ internal unsafe abstract class BufferBase
+ {
+ public int Size { get; protected set; }
+ public int CurrentIndex { get; protected set; }
+ public byte* Address { get; protected set; }
+
+ ///
+ /// Ensure memory safety and advance position by specified number of bytes
+ ///
+ ///
+ /// Pointer to the current position in the buffer
+ protected abstract byte* Alloc(int cbSize);
+
+ protected T* Alloc(int count = 1) where T : unmanaged
+ => (T*)Alloc(count * sizeof(T));
+ }
+
+ internal unsafe sealed class WriteBuffer : BufferBase
+ {
+ public WriteBuffer(int size)
+ {
+ Resize(size);
+ }
+
+ public void Resize(int size)
+ {
+ if (size < Size)
+ {
+ Size = size;
+ return;
+ }
+
+ var newAddr = (byte*)Marshal.AllocHGlobal(size);
+ if (Address != null)
+ {
+ Buffer.MemoryCopy(Address, newAddr, size, Size);
+ Marshal.FreeHGlobal((IntPtr)Address);
+ }
+ Size = size;
+ Address = newAddr;
+ }
+
+ protected override byte* Alloc(int cbSize)
+ {
+ var index = CurrentIndex;
+ CurrentIndex += cbSize;
+
+ // Resize the buffer by at least 50% if there's no sufficient space
+ if (CurrentIndex > Size)
+ Resize(Math.Max(CurrentIndex + 1, (int)(Size * 1.5f)));
+
+ return Address + index;
+ }
+
+ public void Write(ref T value) where T : unmanaged
+ {
+ var addr = Alloc();
+ *addr = value;
+ }
+
+ ///
+ /// For passing struct smaller than word size (4/8 bytes on 32/64 bit system)
+ ///
+ ///
+ ///
+ public void WriteVal(T value) where T : unmanaged
+ {
+ var addr = Alloc();
+ *addr = value;
+ }
+
+
+ public void Write(ReadOnlySpan str)
+ {
+ // Prefixed by its size in bytes
+ var cbBody = Encoding.UTF8.GetByteCount(str);
+ WriteVal(cbBody);
+
+ // Allocate and write string body
+ var pBody = Alloc(cbBody);
+ Encoding.UTF8.GetBytes(str, new(pBody, cbBody));
+ }
+
+ // Struct in GTA.Math have pack/padding for memory alignment, we don't want it to waste the bandwidth
+
+ public void Write(ref Vector2 vec2)
+ {
+ var faddr = Alloc(2);
+ faddr[0] = vec2.X;
+ faddr[1] = vec2.Y;
+ }
+
+ public void Write(ref Vector3 vec3)
+ {
+ var faddr = Alloc(3);
+ faddr[0] = vec3.X;
+ faddr[1] = vec3.Y;
+ faddr[2] = vec3.Z;
+ }
+
+ public void Write(ref Quaternion quat)
+ {
+ var faddr = Alloc(4);
+ faddr[0] = quat.X;
+ faddr[1] = quat.Y;
+ faddr[2] = quat.Z;
+ faddr[3] = quat.W;
+ }
+ }
+
+ internal unsafe sealed class ReadBuffer : BufferBase
+ {
+ public ReadBuffer(byte* address, int size)
+ {
+ Address = address;
+ Size = size;
+ }
+
+ protected override byte* Alloc(int cbSize)
+ {
+ var index = CurrentIndex;
+ CurrentIndex += cbSize;
+
+ if (CurrentIndex > Size)
+ throw new InvalidOperationException("Attempting to read beyond the existing buffer");
+
+ return Address + index;
+ }
+
+
+ public T ReadVal() where T : unmanaged => *Alloc();
+
+ public void Read(out T result) where T : unmanaged
+ => result = *Alloc();
+
+ public void Read(out string str)
+ {
+ var cbBody = ReadVal();
+ str = Encoding.UTF8.GetString(Alloc(cbBody), cbBody);
+ }
+
+ public void Read(out Vector2 vec)
+ {
+ var faddr = Alloc(2);
+ vec = new()
+ {
+ X = faddr[0],
+ Y = faddr[1],
+ };
+ }
+
+ public void Read(out Vector3 vec)
+ {
+ var faddr = Alloc(3);
+ vec = new()
+ {
+ X = faddr[0],
+ Y = faddr[1],
+ Z = faddr[2],
+ };
+ }
+
+ public void Read(out Quaternion quat)
+ {
+ var faddr = Alloc(4);
+ quat = new()
+ {
+ X = faddr[0],
+ Y = faddr[1],
+ Z = faddr[2],
+ W = faddr[3],
+ };
+ }
+ }
+}
diff --git a/Core/CoreUtils.cs b/Core/CoreUtils.cs
index d13a03f..7f5bd47 100644
--- a/Core/CoreUtils.cs
+++ b/Core/CoreUtils.cs
@@ -22,6 +22,7 @@ using RageCoop.Core.Scripting;
[assembly: InternalsVisibleTo("RageCoop.Client")]
[assembly: InternalsVisibleTo("RageCoop.Client.Installer")]
[assembly: InternalsVisibleTo("DataDumper")]
+[assembly: InternalsVisibleTo("UnitTest")]
[assembly: InternalsVisibleTo("RageCoop.ResourceBuilder")]
namespace RageCoop.Core
diff --git a/Tools/UnitTest/Program.cs b/Tools/UnitTest/Program.cs
new file mode 100644
index 0000000..8ea0479
--- /dev/null
+++ b/Tools/UnitTest/Program.cs
@@ -0,0 +1,74 @@
+using GTA.Math;
+using RageCoop.Core;
+using System.Security.Cryptography;
+
+namespace UnitTest
+{
+ class TestElement
+ {
+ static SHA256 sha = SHA256.Create();
+ static int blah = new Random().Next();
+ public TestElement(int i)
+ {
+ num = (i + blah) * 1024;
+ vec2 = new(num * 10, num * 20);
+ vec3 = new(num * 10, num * 20, num * 30);
+ quat = new(num * 10, num * 20, num * 30, num * 40);
+ str = sha.ComputeHash(BitConverter.GetBytes(num)).Dump();
+ }
+ public int num;
+ public Vector2 vec2;
+ public Vector3 vec3;
+ public Quaternion quat;
+ public string str;
+ }
+ internal unsafe class Program
+ {
+ static void Main(string[] args)
+ {
+ TestElement[] test = new TestElement[1024];
+ Console.WriteLine("Testing buffers");
+ var buf = new WriteBuffer(1024);
+ for (int i = 0; i < 1024; i++)
+ {
+ var e = test[i] = new TestElement(i);
+ buf.WriteVal(e.num);
+ buf.Write(ref e.vec2);
+ buf.Write(ref e.vec3);
+ buf.Write(ref e.quat);
+ buf.Write(e.str);
+ }
+ Console.WriteLine($"Buffer size: {buf.Size}");
+ Console.WriteLine($"Current position: {buf.CurrentIndex}");
+
+ Console.WriteLine("Validating data");
+ var reader = new ReadBuffer(buf.Address, buf.Size);
+ for (int i = 0; i < 1024; i++)
+ {
+ var e = test[i];
+ reader.Read(out int num);
+ reader.Read(out Vector2 vec2);
+ reader.Read(out Vector3 vec3);
+ reader.Read(out Quaternion quat);
+ reader.Read(out string str);
+
+ if (num != e.num)
+ throw new Exception("POCO fail");
+
+ if (vec2 != e.vec2)
+ throw new Exception("vec2 fail");
+
+ if (vec3 != e.vec3)
+ throw new Exception("vec3 fail");
+
+ if (quat != e.quat)
+ throw new Exception("quat fail");
+
+ if (str != e.str)
+ throw new Exception("str fail");
+ }
+
+ Console.WriteLine("Buffers OK");
+ }
+ }
+}
\ No newline at end of file
diff --git a/Tools/UnitTest/UnitTest.csproj b/Tools/UnitTest/UnitTest.csproj
new file mode 100644
index 0000000..436cc68
--- /dev/null
+++ b/Tools/UnitTest/UnitTest.csproj
@@ -0,0 +1,20 @@
+
+
+
+ Exe
+ net7.0
+ enable
+ True
+
+
+
+
+
+
+
+
+ ..\..\libs\ScriptHookVDotNetCore.dll
+
+
+
+
diff --git a/libs/ScriptHookVDotNetCore.dll b/libs/ScriptHookVDotNetCore.dll
index 3b050f5..d93a544 120000
Binary files a/libs/ScriptHookVDotNetCore.dll and b/libs/ScriptHookVDotNetCore.dll differ