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",