diff --git a/Directory.Build.props b/Directory.Build.props
index 2558bfbe..45263613 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -14,7 +14,7 @@
MIT
© Meta Vens and contributors
true
- 1.0.21
+ 1.0.22
true
diff --git a/src/GameCult.Mesh/CultMeshFrameBodies.cs b/src/GameCult.Mesh/CultMeshFrameBodies.cs
index e01a0901..5c4d26d0 100644
--- a/src/GameCult.Mesh/CultMeshFrameBodies.cs
+++ b/src/GameCult.Mesh/CultMeshFrameBodies.cs
@@ -16,12 +16,16 @@ namespace GameCult.Mesh
// Consumers own only read leases; mappings, capabilities, and slot selection remain CultMesh state.
public sealed class CultMeshFrameBodyPublisher : IDisposable
{
- private const string CapabilityPrefix = "frame-v1-";
- private const int HeaderSize = 128;
- private const int SequenceOffset = 8;
- private const int ReaderCountOffset = 32;
- private const int WriterCountOffset = 48;
- private const int ContractHashOffset = 64;
+ internal const string CapabilityPrefix = "frame-v2-";
+ internal const int HeaderSize = 512;
+ internal const int SequenceOffset = 8;
+ internal const int ReaderCountOffset = 32;
+ internal const int WriterCountOffset = 48;
+ internal const int ContractHashOffset = 64;
+ internal const int ByteLengthOffset = 128;
+ internal const int LeaseExpiresAtOffset = 144;
+ internal const int SemanticHashOffset = 192;
+ internal const int SemanticHashStride = 64;
private readonly object _gate = new();
private readonly MemoryMappedFile _mapping;
private readonly MemoryMappedViewAccessor _control;
@@ -59,6 +63,8 @@ namespace GameCult.Mesh
_control = _mapping.CreateViewAccessor(0, HeaderSize, MemoryMappedFileAccess.ReadWrite);
_control.Write(0, slotByteLength);
_control.Write(4, CultMeshFrameRegion.SlotCount);
+ for (var slot = 0; slot < CultMeshFrameRegion.SlotCount; slot++)
+ _control.Write(SequenceOffset + slot * sizeof(long), -1L);
var contractHash = Encoding.ASCII.GetBytes(ContractHash(BodyId, SchemaId, LayoutVersion, Capacity, ProducerEpoch, SlotByteLength));
_control.WriteArray(ContractHashOffset, contractHash, 0, contractHash.Length);
_mutex = new Mutex(false, MutexName(_token));
@@ -160,6 +166,10 @@ namespace GameCult.Mesh
_control.ReadInt32(ReaderCountOffset + lease.SlotIndex * sizeof(int)) != 0)
throw new InvalidOperationException("The CultMesh frame write reservation was lost before commit.");
_control.Write(SequenceOffset + lease.SlotIndex * sizeof(long), lease.Sequence);
+ _control.Write(ByteLengthOffset + lease.SlotIndex * sizeof(int), byteLength);
+ _control.Write(LeaseExpiresAtOffset + lease.SlotIndex * sizeof(long), nowUtc.Add(_leaseDuration).ToUnixTimeMilliseconds());
+ var hashBytes = Encoding.ASCII.GetBytes(semanticHash);
+ _control.WriteArray(SemanticHashOffset + lease.SlotIndex * SemanticHashStride, hashBytes, 0, hashBytes.Length);
_control.Write(writerOffset, 0);
});
var descriptor = Descriptor(lease.SlotIndex, byteLength, lease.Sequence, semanticHash, nowUtc);
@@ -249,7 +259,7 @@ namespace GameCult.Mesh
finally { mutex.Dispose(); }
}
- private static void ReleaseReader(string token, int slot)
+ internal static void ReleaseReader(string token, int slot)
{
try
{
@@ -281,16 +291,16 @@ namespace GameCult.Mesh
}
private void WithMutex(Action action) => WithMutex(_mutex, action);
- private static void WithMutex(Mutex mutex, Action action)
+ internal static void WithMutex(Mutex mutex, Action action)
{
mutex.WaitOne();
try { action(); }
finally { mutex.ReleaseMutex(); }
}
- private static long SlotOffset(int slot, int slotByteLength) => HeaderSize + (long)slot * slotByteLength;
- private static string MappingName(string token) => "cultmesh-map-" + token;
- private static string MutexName(string token) => "cultmesh-lock-" + token;
- private static void ParseCapability(string capability, out string token, out int slot)
+ internal static long SlotOffset(int slot, int slotByteLength) => HeaderSize + (long)slot * slotByteLength;
+ internal static string MappingName(string token) => "cultmesh-map-" + token;
+ internal static string MutexName(string token) => "cultmesh-lock-" + token;
+ internal static void ParseCapability(string capability, out string token, out int slot)
{
var separator = capability.LastIndexOf('.');
if (separator <= CapabilityPrefix.Length || !int.TryParse(capability.Substring(separator + 1), out slot))
@@ -303,13 +313,180 @@ namespace GameCult.Mesh
using (var random = RandomNumberGenerator.Create()) random.GetBytes(bytes);
return BitConverter.ToString(bytes).Replace("-", string.Empty).ToLowerInvariant();
}
- private static string ContractHash(string bodyId, string schemaId, int layoutVersion, int capacity, long epoch, int slotByteLength) =>
+ internal static string ContractHash(string bodyId, string schemaId, int layoutVersion, int capacity, long epoch, int slotByteLength) =>
CultMeshBodyDescriptorValidator.ComputeSemanticHash(
Encoding.UTF8.GetBytes($"{bodyId}\n{schemaId}\n{layoutVersion}\n{capacity}\n{epoch}\n{slotByteLength}"));
private void ThrowIfDisposed() { if (_disposed) throw new ObjectDisposedException(nameof(CultMeshFrameBodyPublisher)); }
private static string Require(string value, string name) => string.IsNullOrWhiteSpace(value) ? throw new ArgumentException("Value is required.", name) : value;
}
+ ///
+ /// Retains one verified shared-memory frame mapping and leases only newer committed generations.
+ /// The control plane is needed for bootstrap and layout changes, not for every frame.
+ ///
+ public sealed class CultMeshMappedFrameBodyCursor : IDisposable
+ {
+ private readonly CultMeshBodyDescriptor _contract;
+ private readonly string _token;
+ private readonly MemoryMappedFile _mapping;
+ private readonly MemoryMappedViewAccessor _control;
+ private readonly Mutex _mutex;
+ private readonly int _slotByteLength;
+ private long _lastSequence = -1;
+ private bool _disposed;
+
+ public static bool CanOpen(CultMeshBodyDescriptor descriptor) =>
+ descriptor != null &&
+ descriptor.TransportKind == CultMeshBodyTransportKind.SharedMemory &&
+ descriptor.Synchronization == CultMeshBodySynchronization.TripleBuffer &&
+ CultMeshFrameBodyPublisher.IsFrameCapability(descriptor.CapabilityToken);
+
+ public CultMeshMappedFrameBodyCursor(CultMeshBodyDescriptor descriptor)
+ {
+ _contract = descriptor ?? throw new ArgumentNullException(nameof(descriptor));
+ if (!CanOpen(descriptor))
+ throw new ArgumentException("Descriptor is not a mapped CultMesh frame capability.", nameof(descriptor));
+ CultMeshFrameBodyPublisher.ParseCapability(descriptor.CapabilityToken, out _token, out _);
+ try
+ {
+ _mapping = MemoryMappedFile.OpenExisting(
+ CultMeshFrameBodyPublisher.MappingName(_token),
+ MemoryMappedFileRights.ReadWrite);
+ _mutex = Mutex.OpenExisting(CultMeshFrameBodyPublisher.MutexName(_token));
+ }
+ catch (Exception error) when (error is FileNotFoundException || error is WaitHandleCannotBeOpenedException)
+ {
+ throw new InvalidDataException("CultMesh frame capability is unavailable or revoked.", error);
+ }
+ try
+ {
+ _control = _mapping.CreateViewAccessor(
+ 0,
+ CultMeshFrameBodyPublisher.HeaderSize,
+ MemoryMappedFileAccess.ReadWrite);
+ _slotByteLength = _control.ReadInt32(0);
+ var contractHash = new byte[64];
+ _control.ReadArray(CultMeshFrameBodyPublisher.ContractHashOffset, contractHash, 0, contractHash.Length);
+ var expected = CultMeshFrameBodyPublisher.ContractHash(
+ descriptor.BodyId,
+ descriptor.SchemaId,
+ descriptor.LayoutVersion,
+ descriptor.Capacity,
+ descriptor.ProducerEpoch,
+ _slotByteLength);
+ if (!string.Equals(Encoding.ASCII.GetString(contractHash), expected, StringComparison.Ordinal))
+ throw new InvalidDataException("CultMesh frame cursor contract does not match its mapping.");
+ }
+ catch
+ {
+ _mapping.Dispose();
+ _mutex.Dispose();
+ throw;
+ }
+ }
+
+ public long LastSequence => _lastSequence;
+
+ public bool TryAcquireLatest(out ICultMeshBodyReadLease lease) =>
+ TryAcquireLatest(DateTimeOffset.UtcNow, out lease);
+
+ public bool TryAcquireLatest(DateTimeOffset nowUtc, out ICultMeshBodyReadLease lease)
+ {
+ if (_disposed) throw new ObjectDisposedException(nameof(CultMeshMappedFrameBodyCursor));
+ var slot = -1;
+ var sequence = -1L;
+ var byteLength = 0;
+ var leaseExpiresAt = 0L;
+ var semanticHash = "";
+ CultMeshFrameBodyPublisher.WithMutex(_mutex, () =>
+ {
+ var slotCount = _control.ReadInt32(4);
+ for (var candidate = 0; candidate < slotCount; candidate++)
+ {
+ if (_control.ReadInt32(CultMeshFrameBodyPublisher.WriterCountOffset + candidate * sizeof(int)) != 0)
+ continue;
+ var candidateSequence = _control.ReadInt64(
+ CultMeshFrameBodyPublisher.SequenceOffset + candidate * sizeof(long));
+ if (candidateSequence <= _lastSequence || candidateSequence <= sequence)
+ continue;
+ var candidateExpiry = _control.ReadInt64(
+ CultMeshFrameBodyPublisher.LeaseExpiresAtOffset + candidate * sizeof(long));
+ var candidateLength = _control.ReadInt32(
+ CultMeshFrameBodyPublisher.ByteLengthOffset + candidate * sizeof(int));
+ // A byte-length change means the control-plane layout contract changed.
+ // Do not expose that generation through a cursor bootstrapped against
+ // the previous layout; its new descriptor must arrive first.
+ if (candidateExpiry <= nowUtc.ToUnixTimeMilliseconds() ||
+ candidateLength != _contract.ByteSize || candidateLength > _slotByteLength)
+ continue;
+ slot = candidate;
+ sequence = candidateSequence;
+ leaseExpiresAt = candidateExpiry;
+ byteLength = candidateLength;
+ }
+ if (slot < 0) return;
+ var hashBytes = new byte[CultMeshFrameBodyPublisher.SemanticHashStride];
+ _control.ReadArray(
+ CultMeshFrameBodyPublisher.SemanticHashOffset + slot * CultMeshFrameBodyPublisher.SemanticHashStride,
+ hashBytes,
+ 0,
+ hashBytes.Length);
+ semanticHash = Encoding.ASCII.GetString(hashBytes);
+ var readerOffset = CultMeshFrameBodyPublisher.ReaderCountOffset + slot * sizeof(int);
+ _control.Write(readerOffset, checked(_control.ReadInt32(readerOffset) + 1));
+ });
+ if (slot < 0)
+ {
+ lease = null!;
+ return false;
+ }
+
+ var descriptor = new CultMeshBodyDescriptor
+ {
+ BodyId = _contract.BodyId,
+ SchemaId = _contract.SchemaId,
+ LayoutVersion = _contract.LayoutVersion,
+ ByteSize = byteLength,
+ Capacity = _contract.Capacity,
+ ProducerEpoch = _contract.ProducerEpoch,
+ Sequence = sequence,
+ AccessMode = CultMeshBodyAccessMode.ReadOnly,
+ Synchronization = CultMeshBodySynchronization.TripleBuffer,
+ LeaseExpiresAtUnixMs = leaseExpiresAt,
+ TransportKind = CultMeshBodyTransportKind.SharedMemory,
+ CapabilityToken = _token + "." + slot,
+ SemanticHash = semanticHash
+ };
+ try
+ {
+ var mapping = MemoryMappedFile.OpenExisting(
+ CultMeshFrameBodyPublisher.MappingName(_token),
+ MemoryMappedFileRights.ReadWrite);
+ lease = new CultMeshMappedBodyLease(
+ descriptor,
+ mapping,
+ CultMeshFrameBodyPublisher.SlotOffset(slot, _slotByteLength),
+ () => CultMeshFrameBodyPublisher.ReleaseReader(_token, slot));
+ _lastSequence = sequence;
+ return true;
+ }
+ catch
+ {
+ CultMeshFrameBodyPublisher.ReleaseReader(_token, slot);
+ throw;
+ }
+ }
+
+ public void Dispose()
+ {
+ if (_disposed) return;
+ _disposed = true;
+ _control.Dispose();
+ _mapping.Dispose();
+ _mutex.Dispose();
+ }
+ }
+
/// A bounded producer lease over one directly writable shared-memory frame slot.
public sealed unsafe class CultMeshFrameBodyWriteLease : IDisposable
{
diff --git a/src/GameCult.Mesh/README.md b/src/GameCult.Mesh/README.md
index 2f80d345..8f118320 100644
--- a/src/GameCult.Mesh/README.md
+++ b/src/GameCult.Mesh/README.md
@@ -380,6 +380,40 @@ runtime-native storage: shared D3D12 textures, shared memory rings, platform GPU
handles, DMA buffers, or paged CultCache fallbacks when a zero-copy transport is
not available.
+For entity SoA and other CPU-visible hot state, subscribe to the exact control
+records and logical bodies the lowerer consumes. The subscription advertises
+the transports the runtime can import; the provider's demand tracker selects
+the cheapest mutually usable representation. A same-machine consumer normally
+receives one shared-memory bootstrap descriptor, then observes later commits
+through the mapped ring header. Per-frame CultNet documents are neither needed
+nor desirable.
+
+```csharp
+var initial = await subscriptions.SubscribeAsync(
+ "pilot-world",
+ recordKeys: new[] { entityLayoutRecord, bodyPublicationRecord },
+ schemaIds: new[] { entityLayoutSchema, CultMeshBodyPublicationSchemaVersions.Publication },
+ consumerRuntimeId: "eve-unity",
+ bodyIds: new[] { "eve:entity-soa:aetheria.daemon:pilot" },
+ supportedBodyTransports: new[]
+ {
+ CultMeshBodyTransportKind.SharedMemory.ToString(),
+ CultMeshBodyTransportKind.Network.ToString()
+ });
+
+var bootstrap = initial.OfType().Single();
+var mapped = bootstrap.Representations.Single(CultMeshMappedFrameBodyCursor.CanOpen);
+using var cursor = new CultMeshMappedFrameBodyCursor(mapped);
+
+// Call from the consumer's normal update loop. The returned lease is read-only
+// and must remain alive until the renderer has finished consuming that frame.
+if (cursor.TryAcquireLatest(out var frame))
+ Present(frame);
+```
+
+Snapshots remain bootstrap and recovery machinery. Reactive document changes
+carry small control state; mapped or negotiated network bodies carry hot data.
+
```csharp
using GameCult.Mesh;
diff --git a/src/GameCult.Networking/CultNetDatabaseSubscriptionClient.cs b/src/GameCult.Networking/CultNetDatabaseSubscriptionClient.cs
index cfe349c5..d0a545c0 100644
--- a/src/GameCult.Networking/CultNetDatabaseSubscriptionClient.cs
+++ b/src/GameCult.Networking/CultNetDatabaseSubscriptionClient.cs
@@ -40,6 +40,10 @@ namespace GameCult.Networking
/// Gets the local reactive cache receiving replicated documents.
public CultCache Cache => _cache;
+ /// Completes if the retained transport pump terminates with an unrecoverable error.
+ public Task? BackgroundFailure =>
+ (_client as RudpCultNetSchemaClient)?.BackgroundFailure;
+
///
/// Starts a remote subscription and waits until its initial matching snapshot is in the local cache.
///
diff --git a/tests/GameCult.Mesh.Tests/CultMeshFrameBodyTests.cs b/tests/GameCult.Mesh.Tests/CultMeshFrameBodyTests.cs
index 4ecbe339..2d69015d 100644
--- a/tests/GameCult.Mesh.Tests/CultMeshFrameBodyTests.cs
+++ b/tests/GameCult.Mesh.Tests/CultMeshFrameBodyTests.cs
@@ -30,6 +30,82 @@ public sealed class CultMeshFrameBodyTests
lease.ReadByte(0).Should().Be(1);
}
+ [Test]
+ public void MappedCursorObservesNewGenerationsWithoutNewControlDescriptors()
+ {
+ using var publisher = Publisher();
+ publisher.TryPublish(new byte[] { 1, 2, 3 }, DateTimeOffset.UtcNow, out var bootstrap).Should().BeTrue();
+ using var cursor = new CultMeshMappedFrameBodyCursor(bootstrap);
+
+ cursor.TryAcquireLatest(out var first).Should().BeTrue();
+ using (first)
+ {
+ first.Descriptor.Sequence.Should().Be(bootstrap.Sequence);
+ first.ReadByte(0).Should().Be(1);
+ }
+ cursor.TryAcquireLatest(out _).Should().BeFalse();
+
+ publisher.TryPublish(new byte[] { 9, 8, 7 }, DateTimeOffset.UtcNow, out _).Should().BeTrue();
+
+ cursor.TryAcquireLatest(out var second).Should().BeTrue();
+ using (second)
+ {
+ second.Descriptor.Sequence.Should().Be(bootstrap.Sequence + 1);
+ second.Descriptor.ByteSize.Should().Be(3);
+ second.ReadByte(0).Should().Be(9);
+ }
+ cursor.TryAcquireLatest(out _).Should().BeFalse();
+ }
+
+ [Test]
+ public void MappedCursorWaitsForNewControlContractWhenFrameLengthChanges()
+ {
+ using var publisher = Publisher();
+ publisher.TryPublish(new byte[] { 1 }, DateTimeOffset.UtcNow, out var bootstrap).Should().BeTrue();
+ using var cursor = new CultMeshMappedFrameBodyCursor(bootstrap);
+ cursor.TryAcquireLatest(out var first).Should().BeTrue();
+ first.Dispose();
+
+ publisher.TryPublish(new byte[] { 2, 3 }, DateTimeOffset.UtcNow, out _).Should().BeTrue();
+
+ cursor.TryAcquireLatest(out _).Should().BeFalse();
+ }
+
+ [Test]
+ public void MappedCursorReaderLeaseProtectsItsSlotUntilDisposed()
+ {
+ using var publisher = Publisher();
+ publisher.TryPublish(new byte[] { 1 }, DateTimeOffset.UtcNow, out var bootstrap).Should().BeTrue();
+ using var cursor = new CultMeshMappedFrameBodyCursor(bootstrap);
+ cursor.TryAcquireLatest(out var protectedRead).Should().BeTrue();
+
+ publisher.TryPublish(new byte[] { 2 }, DateTimeOffset.UtcNow, out _).Should().BeTrue();
+ publisher.TryPublish(new byte[] { 3 }, DateTimeOffset.UtcNow, out _).Should().BeTrue();
+ publisher.TryPublish(new byte[] { 4 }, DateTimeOffset.UtcNow, out var fourth).Should().BeTrue();
+
+ fourth.CapabilityToken.Should().NotBe(bootstrap.CapabilityToken);
+ protectedRead.ReadByte(0).Should().Be(1);
+ protectedRead.Dispose();
+ }
+
+ [Test]
+ public void MappedCursorIgnoresExpiredGenerations()
+ {
+ var publishedAt = DateTimeOffset.UtcNow;
+ using var publisher = new CultMeshFrameBodyPublisher(
+ "body",
+ "tests.frame.v2",
+ layoutVersion: 2,
+ capacity: 64,
+ producerEpoch: 9,
+ slotByteLength: 64,
+ leaseDuration: TimeSpan.FromMilliseconds(10));
+ publisher.TryPublish(new byte[] { 42 }, publishedAt, out var bootstrap).Should().BeTrue();
+ using var cursor = new CultMeshMappedFrameBodyCursor(bootstrap);
+
+ cursor.TryAcquireLatest(publishedAt.AddMilliseconds(11), out _).Should().BeFalse();
+ }
+
[TestCase("wrong", 2, 9)]
[TestCase("tests.frame.v2", 1, 9)]
[TestCase("tests.frame.v2", 2, 8)]
diff --git a/unity/org.gamecult.cultlib/README.md b/unity/org.gamecult.cultlib/README.md
index b7255bbe..be09e204 100644
--- a/unity/org.gamecult.cultlib/README.md
+++ b/unity/org.gamecult.cultlib/README.md
@@ -7,8 +7,8 @@ CultCache, CultNet, and CultMesh assemblies consumed through NuGet by normal
Build it with `scripts/build-unity-package.ps1`. Do not edit generated plugin
assemblies in the package output.
-Install version `1.0.8` through Unity Package Manager with:
+Install version `1.0.22` through Unity Package Manager with:
```text
-https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.8
+https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.22
```
diff --git a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Caching.MessagePack.dll b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Caching.MessagePack.dll
index e69348f1..d13a4ef8 100644
Binary files a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Caching.MessagePack.dll and b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Caching.MessagePack.dll differ
diff --git a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Caching.MessagePack.pdb b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Caching.MessagePack.pdb
index 20990aa3..a55bee74 100644
Binary files a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Caching.MessagePack.pdb and b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Caching.MessagePack.pdb differ
diff --git a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Caching.dll b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Caching.dll
index 9a1e1997..caa426d0 100644
Binary files a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Caching.dll and b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Caching.dll differ
diff --git a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Caching.pdb b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Caching.pdb
index bbb283d4..fb55549d 100644
Binary files a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Caching.pdb and b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Caching.pdb differ
diff --git a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Logging.dll b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Logging.dll
index e4f2a5c8..75d4d505 100644
Binary files a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Logging.dll and b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Logging.dll differ
diff --git a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Logging.pdb b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Logging.pdb
index af69cc78..96c3418c 100644
Binary files a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Logging.pdb and b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Logging.pdb differ
diff --git a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Mesh.dll b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Mesh.dll
index d192ed78..85a1216f 100644
Binary files a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Mesh.dll and b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Mesh.dll differ
diff --git a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Mesh.pdb b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Mesh.pdb
index bcc1f7ed..6faa3ece 100644
Binary files a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Mesh.pdb and b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Mesh.pdb differ
diff --git a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Networking.dll b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Networking.dll
index f43687c3..919bf200 100644
Binary files a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Networking.dll and b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Networking.dll differ
diff --git a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Networking.pdb b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Networking.pdb
index 033fbee0..db888d0e 100644
Binary files a/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Networking.pdb and b/unity/org.gamecult.cultlib/Runtime/Plugins/GameCult.Networking.pdb differ
diff --git a/unity/org.gamecult.cultlib/package.json b/unity/org.gamecult.cultlib/package.json
index 8c2e6811..3633a675 100644
--- a/unity/org.gamecult.cultlib/package.json
+++ b/unity/org.gamecult.cultlib/package.json
@@ -1,7 +1,7 @@
{
"name": "org.gamecult.cultlib",
"displayName": "CultLib",
- "version": "1.0.21",
+ "version": "1.0.22",
"unity": "2021.3",
"description": "CultCache, CultNet, and CultMesh managed runtime assemblies for Unity.",
"license": "MIT",