diff --git a/Directory.Build.props b/Directory.Build.props
index a05e05bb..0817976e 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -14,7 +14,7 @@
MIT
© Meta Vens and contributors
true
- 1.0.27
+ 1.0.28
true
diff --git a/src/GameCult.Mesh/CultMesh.cs b/src/GameCult.Mesh/CultMesh.cs
index 567de868..7efa0aa1 100644
--- a/src/GameCult.Mesh/CultMesh.cs
+++ b/src/GameCult.Mesh/CultMesh.cs
@@ -5,6 +5,7 @@ using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Reflection;
+using System.Threading;
using System.Threading.Tasks;
using GameCult.Caching;
using GameCult.Caching.MessagePack;
@@ -2889,6 +2890,51 @@ namespace GameCult.Mesh
return SubscribeHotBodyAsync(subscriptions, bodyResolver.SupportedTransports, subscription, deliveryMode);
}
+ ///
+ /// Subscribes to one logical body's latest descriptor as ephemeral control state and declares
+ /// demand for the body itself. Body bytes are opened separately through the fastest valid plane.
+ ///
+ public static async Task SubscribeLiveBodyAsync(
+ CultNetDatabaseSubscriptionClient subscriptions,
+ CultMeshBodyPublicationResolver bodyResolver,
+ CultMeshLiveBodySubscription subscription,
+ CancellationToken cancellationToken = default)
+ {
+ if (subscriptions == null) throw new ArgumentNullException(nameof(subscriptions));
+ if (bodyResolver == null) throw new ArgumentNullException(nameof(bodyResolver));
+ if (subscription == null) throw new ArgumentNullException(nameof(subscription));
+ var transports = bodyResolver.SupportedTransports.Distinct().ToArray();
+ if (transports.Length == 0)
+ throw new InvalidOperationException("The body resolver has no readable transport planes.");
+
+ var liveBody = new CultMeshLiveBody(subscriptions, bodyResolver, subscription);
+ try
+ {
+ var initial = await subscriptions.SubscribeAsync(
+ subscription.SubscriptionId,
+ recordKeys: new[] { subscription.PublicationRecordKey },
+ schemaIds: new[] { CultMeshBodyPublicationSchemaVersions.Publication },
+ consumerRuntimeId: subscription.ConsumerRuntimeId,
+ bodyIds: new[] { subscription.BodyId },
+ supportedBodyTransports: transports.Select(value => value.ToString()),
+ deliveryMode: CultNetDatabaseSubscriptionDeliveryMode.Live,
+ cancellationToken: cancellationToken)
+ .ConfigureAwait(false);
+ var publications = initial.OfType().ToArray();
+ if (publications.Length != 1)
+ throw new InvalidOperationException(
+ $"Live body subscription '{subscription.SubscriptionId}' expected one publication for " +
+ $"'{subscription.BodyId}' but received {publications.Length}.");
+ liveBody.Initialize(publications[0]);
+ return liveBody;
+ }
+ catch
+ {
+ liveBody.Dispose();
+ throw;
+ }
+ }
+
/// Resolves a CultMesh URI or RUDP endpoint into a concrete RUDP endpoint.
public static CultMeshRudpEndpoint ResolveRudpEndpoint(string endpointOrUri)
{
diff --git a/src/GameCult.Mesh/CultMeshBodyDemand.cs b/src/GameCult.Mesh/CultMeshBodyDemand.cs
index 7bfc230f..416cc13c 100644
--- a/src/GameCult.Mesh/CultMeshBodyDemand.cs
+++ b/src/GameCult.Mesh/CultMeshBodyDemand.cs
@@ -7,6 +7,169 @@ using GameCult.Networking;
namespace GameCult.Mesh
{
+ ///
+ /// Declares one logical body demand. CultMesh owns locality detection and transport selection;
+ /// consumers name the body and the planes they can actually read.
+ ///
+ public sealed class CultMeshLiveBodySubscription
+ {
+ public CultMeshLiveBodySubscription(
+ string subscriptionId,
+ string consumerRuntimeId,
+ string bodyId)
+ {
+ SubscriptionId = Require(subscriptionId, nameof(subscriptionId));
+ ConsumerRuntimeId = Require(consumerRuntimeId, nameof(consumerRuntimeId));
+ BodyId = Require(bodyId, nameof(bodyId));
+ }
+
+ public string SubscriptionId { get; }
+ public string ConsumerRuntimeId { get; }
+ public string BodyId { get; }
+ public string PublicationRecordKey =>
+ CultMeshBodyPublicationDocument.CreateLatestRecordKey(BodyId).Value;
+
+ private static string Require(string value, string parameterName) =>
+ string.IsNullOrWhiteSpace(value)
+ ? throw new ArgumentException("Value is required.", parameterName)
+ : value;
+ }
+
+ ///
+ /// One ephemeral body subscription. Publication metadata remains reactive control state while
+ /// body bytes stay on the negotiated body plane.
+ ///
+ public sealed class CultMeshLiveBody : IDisposable
+ {
+ private readonly CultNetDatabaseSubscriptionClient _owner;
+ private readonly CultMeshBodyPublicationResolver _resolver;
+ private readonly CultMeshLiveBodySubscription _subscription;
+ private readonly object _gate = new();
+ private CultMeshBodyPublicationDocument? _current;
+ private bool _hasValue;
+ private bool _disposed;
+
+ internal CultMeshLiveBody(
+ CultNetDatabaseSubscriptionClient owner,
+ CultMeshBodyPublicationResolver resolver,
+ CultMeshLiveBodySubscription subscription)
+ {
+ _owner = owner ?? throw new ArgumentNullException(nameof(owner));
+ _resolver = resolver ?? throw new ArgumentNullException(nameof(resolver));
+ _subscription = subscription ?? throw new ArgumentNullException(nameof(subscription));
+ _owner.Changed += OnChanged;
+ }
+
+ /// Raised when the provider publishes a new generation descriptor.
+ public event Action? Changed;
+
+ /// Raised when the provider withdraws the logical body.
+ public event Action? Removed;
+
+ public string BodyId => _subscription.BodyId;
+
+ public bool HasValue
+ {
+ get { lock (_gate) return _hasValue; }
+ }
+
+ public CultMeshBodyPublicationDocument Current
+ {
+ get
+ {
+ lock (_gate)
+ return _hasValue
+ ? _current!
+ : throw new InvalidOperationException(
+ $"Live body '{_subscription.BodyId}' is not currently published.");
+ }
+ }
+
+ ///
+ /// Opens the current generation read-only using the fastest valid advertised representation.
+ /// The caller owns the returned lease and should dispose it before opening a later generation.
+ ///
+ public CultMeshBodyNegotiationResult OpenCurrentReadOnly(DateTimeOffset? nowUtc = null)
+ {
+ CultMeshBodyPublicationDocument publication;
+ lock (_gate)
+ {
+ if (_disposed) throw new ObjectDisposedException(nameof(CultMeshLiveBody));
+ if (!_hasValue)
+ throw new InvalidOperationException(
+ $"Live body '{_subscription.BodyId}' is not currently published.");
+ publication = _current!;
+ }
+
+ return _resolver.NegotiateReadOnly(publication, new CultMeshBodyValidationRequest
+ {
+ BodyId = publication.BodyId,
+ SchemaId = publication.SchemaId,
+ LayoutVersion = publication.LayoutVersion,
+ ProducerEpoch = publication.ProducerEpoch,
+ Sequence = publication.Sequence,
+ Capacity = publication.Capacity,
+ AccessMode = CultMeshBodyAccessMode.ReadOnly,
+ NowUtc = nowUtc ?? DateTimeOffset.UtcNow
+ });
+ }
+
+ internal void Initialize(CultMeshBodyPublicationDocument publication)
+ {
+ CultMeshBodyPublicationValidator.Validate(publication, expectedBodyId: _subscription.BodyId);
+ lock (_gate)
+ {
+ if (_disposed || _hasValue) return;
+ _current = publication;
+ _hasValue = true;
+ }
+ }
+
+ public void Dispose()
+ {
+ lock (_gate)
+ {
+ if (_disposed) return;
+ _disposed = true;
+ _current = null;
+ _hasValue = false;
+ }
+ _owner.Changed -= OnChanged;
+ _owner.Unsubscribe(_subscription.SubscriptionId);
+ }
+
+ private void OnChanged(CultNetReplicatedDocumentChange change)
+ {
+ if (!string.Equals(change.SubscriptionId, _subscription.SubscriptionId, StringComparison.Ordinal) ||
+ !string.Equals(change.RecordKey, _subscription.PublicationRecordKey, StringComparison.Ordinal) ||
+ !string.Equals(change.SchemaId, CultMeshBodyPublicationSchemaVersions.Publication, StringComparison.Ordinal))
+ return;
+
+ if (string.Equals(change.ChangeKind, "removed", StringComparison.Ordinal))
+ {
+ lock (_gate)
+ {
+ if (_disposed) return;
+ _current = null;
+ _hasValue = false;
+ }
+ Removed?.Invoke();
+ return;
+ }
+
+ if (change.Document is not CultMeshBodyPublicationDocument publication)
+ return;
+ CultMeshBodyPublicationValidator.Validate(publication, expectedBodyId: _subscription.BodyId);
+ lock (_gate)
+ {
+ if (_disposed) return;
+ _current = publication;
+ _hasValue = true;
+ }
+ Changed?.Invoke(publication);
+ }
+ }
+
///
/// One exact hot-body subscription. The view record, latest-publication record, consumer body
/// demand, and locally supported planes are one contract so callers cannot accidentally stream
@@ -68,12 +231,14 @@ namespace GameCult.Mesh
BodyId = bodyId;
Consumers = consumers;
RequiresSharedMemory = consumers.Any(value => value.Transport == CultMeshBodyTransportKind.SharedMemory);
+ RequiresSharedFileMapping = consumers.Any(value => value.Transport == CultMeshBodyTransportKind.SharedFileMapping);
RequiresNetwork = consumers.Any(value => value.Transport == CultMeshBodyTransportKind.Network);
}
public string BodyId { get; }
public bool HasConsumers => Consumers.Count > 0;
public bool RequiresSharedMemory { get; }
+ public bool RequiresSharedFileMapping { get; }
public bool RequiresNetwork { get; }
public IReadOnlyList Consumers { get; }
}
@@ -172,6 +337,11 @@ namespace GameCult.Mesh
demand.ConsumerRuntimeId,
demand.SubscriptionId,
CultMeshBodyTransportKind.SharedMemory);
+ if (demand.SameMachine && Supports(supported, CultMeshBodyTransportKind.SharedFileMapping))
+ return new CultMeshBodyConsumerRoute(
+ demand.ConsumerRuntimeId,
+ demand.SubscriptionId,
+ CultMeshBodyTransportKind.SharedFileMapping);
if (Supports(supported, CultMeshBodyTransportKind.Network))
return new CultMeshBodyConsumerRoute(
demand.ConsumerRuntimeId,
diff --git a/src/GameCult.Mesh/README.md b/src/GameCult.Mesh/README.md
index 8f118320..2539f83e 100644
--- a/src/GameCult.Mesh/README.md
+++ b/src/GameCult.Mesh/README.md
@@ -389,28 +389,29 @@ 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()
- });
+using var entities = await CultMesh.SubscribeLiveBodyAsync(
+ subscriptions,
+ bodyResolver,
+ new CultMeshLiveBodySubscription(
+ "pilot-entities",
+ "eve-unity",
+ "eve:entity-soa:aetheria.daemon:pilot"));
-var bootstrap = initial.OfType().Single();
-var mapped = bootstrap.Representations.Single(CultMeshMappedFrameBodyCursor.CanOpen);
-using var cursor = new CultMeshMappedFrameBodyCursor(mapped);
+using var input = await subscriptions.SubscribeLiveValueAsync(
+ "pilot-input",
+ "eve:input:aetheria.daemon:pilot");
-// 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);
+// Open after each descriptor change. CultMesh chooses shared memory, shared-file
+// mapping, or network from observed locality and the resolver's real adapters.
+using var frame = entities.OpenCurrentReadOnly().Lease;
+Present(frame, input.Current);
```
+`CultMeshLiveBody` watches only the small latest-publication record and declares
+the logical body demand. It never routes the body through the subscription
+snapshot. `CultNetLiveValue` does the corresponding job for exact scalar or
+small structured state without creating a renderer-owned replica cache.
+
Snapshots remain bootstrap and recovery machinery. Reactive document changes
carry small control state; mapped or negotiated network bodies carry hot data.
diff --git a/src/GameCult.Mesh/docs/public-api.md b/src/GameCult.Mesh/docs/public-api.md
index 512b3e30..876a56f6 100644
--- a/src/GameCult.Mesh/docs/public-api.md
+++ b/src/GameCult.Mesh/docs/public-api.md
@@ -520,6 +520,12 @@ explicitly forwarded. Stale epochs fail loudly.
Consumers subscribe to typed document changes through `CultNetDatabase` watch
methods. Subscriptions receive domain changes rather than storage envelopes.
+Exact remote values use `SubscribeLiveValueAsync(...)`; they remain ephemeral
+and own their unsubscribe lifetime. Hot body state uses
+`CultMesh.SubscribeLiveBodyAsync(...)`: the reactive value is only the current
+body descriptor, while `OpenCurrentReadOnly()` negotiates shared memory,
+shared-file mapping, or network from observed locality and available adapters.
+Body bytes never become subscription snapshot payload.
### Client Prediction
diff --git a/src/GameCult.Mesh/docs/reliability-migration-plan.md b/src/GameCult.Mesh/docs/reliability-migration-plan.md
index 515701fb..11437238 100644
--- a/src/GameCult.Mesh/docs/reliability-migration-plan.md
+++ b/src/GameCult.Mesh/docs/reliability-migration-plan.md
@@ -899,7 +899,9 @@ one representation. A renderer owns none of those decisions.
**Inputs:** consumer runtime identity, exact logical body IDs, advertised
consumer transport capabilities, and server-observed peer locality. A
same-machine claim selects shared memory only when the transport peer is
-actually loopback and the consumer explicitly supports shared memory.
+actually loopback and the consumer explicitly supports shared memory. When
+named shared memory is unavailable but the consumer can map files, the same
+locality proof selects shared-file mapping before network.
**Outputs:** a per-body plan requiring shared memory, network, both, or no
publication. Producers use that plan before allocating, copying, hashing, or
@@ -918,7 +920,15 @@ stream demand merely by existing.
and server disposal all update the same typed demand projection. Stable peer
identity, not per-message wrapper identity, keys subscription lifetime.
-`CultMesh.SubscribeHotBodyAsync(...)` is the public construction path. Its
+`CultMesh.SubscribeLiveBodyAsync(...)` is the narrow public construction path.
+Its `CultMeshLiveBodySubscription` names only the logical body, consumer, and
+subscription lifetime. `CultMeshLiveBody` owns the ephemeral latest-publication
+value and opens the current generation through the fastest valid representation
+the resolver can actually import. Layout and other small state are independent
+exact reactive values, so changing one variable does not redeliver an unrelated
+view document or body.
+
+`CultMesh.SubscribeHotBodyAsync(...)` remains the grouped compatibility path. Its
`CultMeshHotBodySubscription` keeps the exact view record, the body's stable
latest-publication record, consumer identity, and logical body demand in one
contract. The local `CultMeshBodyTransportService` contributes only adapters it
diff --git a/tests/GameCult.Mesh.Tests/CultMeshBodyPublicationTests.cs b/tests/GameCult.Mesh.Tests/CultMeshBodyPublicationTests.cs
index f997bdad..afd591ba 100644
--- a/tests/GameCult.Mesh.Tests/CultMeshBodyPublicationTests.cs
+++ b/tests/GameCult.Mesh.Tests/CultMeshBodyPublicationTests.cs
@@ -1,7 +1,11 @@
using System;
+using System.Collections.Generic;
using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
using FluentAssertions;
using GameCult.Caching;
+using GameCult.Networking;
using NUnit.Framework;
#nullable enable
@@ -183,6 +187,39 @@ public sealed class CultMeshBodyPublicationTests
wrongGeneration.Should().Throw().WithMessage("*epoch*");
}
+ [Test]
+ public async Task LiveBody_SubscriptionKeepsBytesOffTheSnapshotAndOpensTheBestPlane()
+ {
+ var now = DateTimeOffset.UtcNow;
+ var bytes = new byte[] { 3, 1, 4, 1, 5, 9 };
+ var publication = Publication(bytes, now);
+ var cache = new CultCache();
+ var documents = CultMesh.CreateBodyPublicationDocumentRegistry(cache.Registry);
+ var transport = new BodyPublicationSchemaClient(publication, documents);
+ using var subscriptions = new CultNetDatabaseSubscriptionClient(transport, cache, documents);
+
+ using var body = await CultMesh.SubscribeLiveBodyAsync(
+ subscriptions,
+ Resolver(bytes, (_, _) => true),
+ new CultMeshLiveBodySubscription("pilot-body", "eve-unity", publication.BodyId));
+
+ transport.Subscribe.Should().NotBeNull();
+ transport.Subscribe!.RecordKeys.Should().Equal(
+ CultMeshBodyPublicationDocument.CreateLatestRecordKey(publication.BodyId).Value);
+ transport.Subscribe.SchemaIds.Should().Equal(CultMeshBodyPublicationSchemaVersions.Publication);
+ transport.Subscribe.BodyIds.Should().Equal(publication.BodyId);
+ transport.Subscribe.ConsumerRuntimeId.Should().Be("eve-unity");
+ cache.Get(
+ CultMeshBodyPublicationDocument.CreateLatestRecordKey(publication.BodyId)).Should().BeNull(
+ "live descriptor control state must not become a renderer-owned replica");
+
+ using var opened = body.OpenCurrentReadOnly(now).Lease;
+ opened.TransportKind.Should().Be(CultMeshBodyTransportKind.SharedFileMapping);
+ var restored = new byte[bytes.Length];
+ opened.CopyTo(0, restored, 0, restored.Length).Should().Be(bytes.Length);
+ restored.Should().Equal(bytes);
+ }
+
private CultMeshBodyPublicationDocument Publication(byte[] bytes, DateTimeOffset now)
{
var local = new CultMeshMappedBodyPublisher(_root, TimeSpan.FromMinutes(1))
@@ -248,4 +285,49 @@ public sealed class CultMeshBodyPublicationTests
CapabilityToken = "cultnet:body:aetheria:entities:7:42",
SemanticHash = local.SemanticHash
};
+
+ private sealed class BodyPublicationSchemaClient : ICultNetSchemaClient
+ {
+ private readonly CultMeshBodyPublicationDocument _publication;
+ private readonly CultNetDocumentRegistry _documents;
+ private readonly List> _snapshots = new();
+
+ public BodyPublicationSchemaClient(
+ CultMeshBodyPublicationDocument publication,
+ CultNetDocumentRegistry documents)
+ {
+ _publication = publication;
+ _documents = documents;
+ }
+
+ public bool Connected => true;
+ public CultNetDatabaseSubscribeMessage? Subscribe { get; private set; }
+ public void Connect(string host, int port) { }
+
+ public void SendCultNet(T message) where T : ICultNetSchemaMessage
+ {
+ if (message is not CultNetDatabaseSubscribeMessage subscribe)
+ return;
+ Subscribe = subscribe;
+ var put = _documents.CreateRawDocumentPutMessage(
+ "body-publication",
+ new CultRecordHandle(
+ CultMeshBodyPublicationDocument.CreateLatestRecordKey(_publication.BodyId)),
+ _publication);
+ var response = new CultNetSnapshotResponseRawMessage
+ {
+ MessageId = subscribe.MessageId,
+ Documents = new[] { put.Document }
+ };
+ foreach (var snapshot in _snapshots.ToArray()) snapshot(response);
+ }
+
+ public void OnCultNet(Action callback) where T : ICultNetSchemaMessage
+ {
+ if (typeof(T) == typeof(CultNetSnapshotResponseRawMessage))
+ _snapshots.Add(response => callback((T)(object)response));
+ }
+
+ public void Dispose() { }
+ }
}
diff --git a/tests/GameCult.Mesh.Tests/CultMeshBodyTransportTests.cs b/tests/GameCult.Mesh.Tests/CultMeshBodyTransportTests.cs
index 11782a0a..e644c3ff 100644
--- a/tests/GameCult.Mesh.Tests/CultMeshBodyTransportTests.cs
+++ b/tests/GameCult.Mesh.Tests/CultMeshBodyTransportTests.cs
@@ -1,6 +1,7 @@
using System;
using System.IO;
using System.Linq;
+using System.Threading.Tasks;
using FluentAssertions;
using NUnit.Framework;
using System.IO.MemoryMappedFiles;
@@ -358,6 +359,48 @@ public sealed class CultMeshBodyTransportTests
plan.Consumers.Should().ContainSingle().Which.ConsumerRuntimeId.Should().Be("unknown");
}
+ [Test]
+ public void BodyDemand_UsesSharedFileMappingAsTheSameMachineFallback()
+ {
+ using var tracker = new CultMeshBodyDemandTracker();
+ tracker.Observe(new CultNetDatabaseSubscriptionDemand(
+ "unity-local",
+ "mapped-local",
+ new[] { "eve:entity-soa:aetheria.daemon:pilot" },
+ new[]
+ {
+ CultMeshBodyTransportKind.SharedFileMapping.ToString(),
+ CultMeshBodyTransportKind.Network.ToString()
+ },
+ sameMachine: true,
+ active: true));
+
+ var plan = tracker.Plan("eve:entity-soa:aetheria.daemon:pilot");
+
+ plan.RequiresSharedMemory.Should().BeFalse();
+ plan.RequiresSharedFileMapping.Should().BeTrue();
+ plan.RequiresNetwork.Should().BeFalse();
+ plan.Consumers.Should().ContainSingle().Which.Transport
+ .Should().Be(CultMeshBodyTransportKind.SharedFileMapping);
+ }
+
+ [Test]
+ public void LiveBodySubscriptionNamesOnlyTheReactiveDescriptorAndLogicalBodyDemand()
+ {
+ var subscription = new CultMeshLiveBodySubscription(
+ "pilot-entities",
+ "eve-unity",
+ "eve:body:pilot");
+
+ subscription.SubscriptionId.Should().Be("pilot-entities");
+ subscription.ConsumerRuntimeId.Should().Be("eve-unity");
+ subscription.BodyId.Should().Be("eve:body:pilot");
+ subscription.PublicationRecordKey.Should().Be(
+ CultMeshBodyPublicationDocument.CreateLatestRecordKey("eve:body:pilot").Value);
+ typeof(CultMesh).GetMethod(nameof(CultMesh.SubscribeLiveBodyAsync))!
+ .ReturnType.Should().Be(typeof(Task));
+ }
+
[Test]
public void HotBodySubscriptionKeepsViewPublicationAndDemandInOneExactContract()
{
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 d6b0b114..2382c1b7 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 d25380aa..95c153bd 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 2850e87d..1f5f832c 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 4ba7fca0..1f1c16ea 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 9b6567f4..a338e289 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 5c04dd08..89e7b13a 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 3fd79cd7..14c28698 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 2bc75c9c..4e448e4a 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 679896cd..15d64a5f 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 8a96379a..e6a6a632 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 f19276c7..36c08d91 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.27",
+ "version": "1.0.28",
"unity": "2021.3",
"description": "CultCache, CultNet, and CultMesh managed runtime assemblies for Unity.",
"license": "MIT",