Route CultNet schema messages through database bridge
This commit is contained in:
parent
b3d9a811e0
commit
6065bb9ad5
7 changed files with 469 additions and 4 deletions
|
|
@ -52,6 +52,7 @@ namespace GameCult.Networking
|
|||
private int _reconnectAttemptCount;
|
||||
|
||||
private readonly ConcurrentDictionary<Type, Delegate> _messageDelegates = new();
|
||||
private readonly ConcurrentDictionary<Type, Delegate> _cultNetMessageDelegates = new();
|
||||
|
||||
private string? _sessionToken;
|
||||
private string _lastHost = "localhost";
|
||||
|
|
@ -137,12 +138,43 @@ namespace GameCult.Networking
|
|||
else Logger.LogError("Cannot send, client is not connected!");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a modern CultNet schema-v0 message when the client is connected and authorized.
|
||||
/// </summary>
|
||||
public void SendCultNet<T>(T message) where T : ICultNetSchemaMessage
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
throw new ObjectDisposedException(nameof(Client));
|
||||
}
|
||||
|
||||
if (message == null) throw new ArgumentNullException(nameof(message));
|
||||
|
||||
if (Connected && _peer != null)
|
||||
{
|
||||
if (Verified || message is CultNetLoginMessage or CultNetRegisterMessage or CultNetVerifyMessage)
|
||||
{
|
||||
Logger.LogDebug($"Sending CultNet schema message {message.SchemaVersion}");
|
||||
_peer.SendCultNet(message);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogError("Cannot send CultNet schema message, client is not verified!");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogError("Cannot send CultNet schema message, client is not connected!");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes all registered message listeners.
|
||||
/// </summary>
|
||||
public void ClearMessageListeners()
|
||||
{
|
||||
_messageDelegates.Clear();
|
||||
_cultNetMessageDelegates.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -172,6 +204,29 @@ namespace GameCult.Networking
|
|||
AddMessageListener(callback);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a listener for a modern CultNet schema-v0 message type.
|
||||
/// </summary>
|
||||
public void AddCultNetMessageListener<T>(Action<T> callback) where T : ICultNetSchemaMessage
|
||||
{
|
||||
var type = typeof(T);
|
||||
_cultNetMessageDelegates.AddOrUpdate(type,
|
||||
_ => callback,
|
||||
(t, current) =>
|
||||
{
|
||||
var combined = Delegate.Combine(current, callback) as Action<T>;
|
||||
return combined ?? throw new InvalidOperationException($"Failed to combine delegates for {t.Name}");
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a listener for a modern CultNet schema-v0 message type.
|
||||
/// </summary>
|
||||
public void OnCultNet<T>(Action<T> callback) where T : ICultNetSchemaMessage
|
||||
{
|
||||
AddCultNetMessageListener(callback);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a previously registered listener for a specific message type.
|
||||
/// </summary>
|
||||
|
|
@ -194,6 +249,26 @@ namespace GameCult.Networking
|
|||
RemoveMessageListener(callback);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a previously registered modern CultNet schema-v0 listener.
|
||||
/// </summary>
|
||||
public void RemoveCultNetMessageListener<T>(Action<T> callback) where T : ICultNetSchemaMessage
|
||||
{
|
||||
if (_cultNetMessageDelegates.TryGetValue(typeof(T), out var currentDelegate))
|
||||
{
|
||||
var newDelegate = Delegate.Remove(currentDelegate, callback) as Action<T>;
|
||||
_cultNetMessageDelegates[typeof(T)] = newDelegate ?? (_ => { });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a previously registered modern CultNet schema-v0 listener.
|
||||
/// </summary>
|
||||
public void OffCultNet<T>(Action<T> callback) where T : ICultNetSchemaMessage
|
||||
{
|
||||
RemoveCultNetMessageListener(callback);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a login request.
|
||||
/// </summary>
|
||||
|
|
@ -293,6 +368,23 @@ namespace GameCult.Networking
|
|||
try
|
||||
{
|
||||
var bytes = reader.GetRemainingBytes();
|
||||
if (TryDeserializeCultNet(bytes, out var cultNetMessage))
|
||||
{
|
||||
Logger.LogDebug($"Received CultNet schema message {cultNetMessage.SchemaVersion}");
|
||||
var cultNetType = cultNetMessage.GetType();
|
||||
if (_cultNetMessageDelegates.TryGetValue(cultNetType, out var cultNetDelegate) &&
|
||||
cultNetDelegate != null)
|
||||
{
|
||||
cultNetDelegate.DynamicInvoke(cultNetMessage);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogWarning($"No listener for CultNet schema message {cultNetMessage.SchemaVersion}");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var message = MessageSerialization.Deserialize<Message>(bytes);
|
||||
if (LogSensitivePayloads)
|
||||
{
|
||||
|
|
@ -355,6 +447,20 @@ namespace GameCult.Networking
|
|||
(peer, latency) => Ping = latency; //Logger($"Ping received: {latency} ms");
|
||||
}
|
||||
|
||||
private static bool TryDeserializeCultNet(byte[] payload, out ICultNetSchemaMessage message)
|
||||
{
|
||||
try
|
||||
{
|
||||
message = CultNetSchemaMessageSerialization.Deserialize(payload);
|
||||
return true;
|
||||
}
|
||||
catch (MessagePackSerializationException)
|
||||
{
|
||||
message = null!;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disconnects the client and suppresses automatic reconnect.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,74 @@
|
|||
# CultNet Distributed Database Notes
|
||||
|
||||
CultNet's distributed database path is a typed realtime database over CultCache
|
||||
documents. The design should borrow proven distributed-systems ideas without
|
||||
copying their entire machine or laundering vague consistency through impressive
|
||||
words.
|
||||
|
||||
## Prior Art To Keep
|
||||
|
||||
- RethinkDB changefeeds: subscriptions should emit old/new document views and
|
||||
support point, table/schema, and filtered watches.
|
||||
- Firebase Realtime Database: client ergonomics should make realtime sync feel
|
||||
like the default path, but presence, disconnect handling, and server-side
|
||||
authority still need explicit ownership.
|
||||
- Dynamo: partitioning, replication metadata, vector-ish causality, hinted
|
||||
recovery, and application-visible conflict resolution are useful. "Always
|
||||
writeable" is not the first CultNet promise.
|
||||
- Raft: primary authority per shard is the first understandable consistency
|
||||
model. Leader election/log replication can come later if the cluster needs
|
||||
automatic failover.
|
||||
- SWIM: peer membership and failure suspicion should be gossip-shaped when the
|
||||
mesh grows beyond small static clusters.
|
||||
- CRDTs: offline multi-writer documents are valid only for types with declared,
|
||||
deterministic merge semantics.
|
||||
- Hashgraph: gossip-about-gossip is interesting for compact event provenance
|
||||
and fair ordering research. CultNet should not adopt public-ledger machinery
|
||||
or crypto-token assumptions as a shortcut to database correctness.
|
||||
|
||||
## Current Cut
|
||||
|
||||
Implemented:
|
||||
|
||||
- `CultNetDatabase`: database-style facade over a `CultCache`
|
||||
- primary-shard write checks
|
||||
- typed R3 watch streams
|
||||
- raw CultNet put/delete application through shard policy
|
||||
- `CultNetDatabaseServer`: schema-v0 bridge for snapshot, put, and delete
|
||||
- `Client` and `Server` schema-v0 dispatch hooks
|
||||
- `CultNetHost.Database` and `CultNetHost.DatabaseServer`
|
||||
|
||||
Not implemented yet:
|
||||
|
||||
- remote subscription request/stream messages
|
||||
- shard catalog exchange
|
||||
- membership/failure detection
|
||||
- leader election or automatic shard failover
|
||||
- replicated log
|
||||
- declared CRDT merge policies
|
||||
|
||||
## Live Invariants
|
||||
|
||||
- CultCache owns document schema, identity, local indexes, and local diffing.
|
||||
- CultNet owns transport, shard authority, remote mutation delivery, and
|
||||
subscription fanout.
|
||||
- One primary owns writes for a shard until a stronger policy exists.
|
||||
- Non-owner writes are rejected or explicitly forwarded. They are not silently
|
||||
applied.
|
||||
- Raw snapshot/put/delete messages must pass through `CultNetDatabase`, not
|
||||
bypass shard policy.
|
||||
- Realtime subscriptions publish domain changes, not storage envelopes.
|
||||
|
||||
## Next Coherent Slice
|
||||
|
||||
Add explicit subscription messages:
|
||||
|
||||
- subscribe by schema
|
||||
- subscribe by record key
|
||||
- subscribe by name/index
|
||||
- unsubscribe
|
||||
- stream domain changes as raw document put/delete records
|
||||
|
||||
That gives clients the RethinkDB/Firebase feel without requiring consensus or
|
||||
membership work first. Shard catalogs and membership should follow once there
|
||||
is more than one authority to discover.
|
||||
114
src/GameCult.Networking/CultNetDatabaseServer.cs
Normal file
114
src/GameCult.Networking/CultNetDatabaseServer.cs
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using LiteNetLib;
|
||||
|
||||
namespace GameCult.Networking
|
||||
{
|
||||
/// <summary>
|
||||
/// Routes CultNet schema-v0 document messages through a <see cref="CultNetDatabase"/>.
|
||||
/// </summary>
|
||||
public sealed class CultNetDatabaseServer : IDisposable
|
||||
{
|
||||
private readonly Server _server;
|
||||
private readonly CultNetDatabase _database;
|
||||
private readonly Func<CultNetSnapshotRequestMessage, NetPeer, Task> _snapshotHandler;
|
||||
private readonly Func<CultNetDocumentPutRawMessage, NetPeer, Task> _putHandler;
|
||||
private readonly Func<CultNetDocumentDeleteMessage, NetPeer, Task> _deleteHandler;
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Creates and attaches a database message bridge to a server.
|
||||
/// </summary>
|
||||
public CultNetDatabaseServer(Server server, CultNetDatabase database)
|
||||
{
|
||||
_server = server ?? throw new ArgumentNullException(nameof(server));
|
||||
_database = database ?? throw new ArgumentNullException(nameof(database));
|
||||
_snapshotHandler = HandleSnapshotRequestAsync;
|
||||
_putHandler = HandlePutAsync;
|
||||
_deleteHandler = HandleDeleteAsync;
|
||||
|
||||
_server.OnCultNet(_snapshotHandler);
|
||||
_server.OnCultNet(_putHandler);
|
||||
_server.OnCultNet(_deleteHandler);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the database used by this bridge.
|
||||
/// </summary>
|
||||
public CultNetDatabase Database => _database;
|
||||
|
||||
/// <summary>
|
||||
/// Applies a raw put message through the database shard policy.
|
||||
/// </summary>
|
||||
public Task<object> ApplyPutAsync(CultNetDocumentPutRawMessage message)
|
||||
{
|
||||
return _database.ApplyPutAsync(message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies a raw delete message through the database shard policy.
|
||||
/// </summary>
|
||||
public Task ApplyDeleteAsync(CultNetDocumentDeleteMessage message)
|
||||
{
|
||||
return _database.ApplyDeleteAsync(message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a raw snapshot response from the database cache.
|
||||
/// </summary>
|
||||
public CultNetSnapshotResponseRawMessage CreateSnapshotResponse(CultNetSnapshotRequestMessage request)
|
||||
{
|
||||
if (request == null) throw new ArgumentNullException(nameof(request));
|
||||
return _database.Documents.CreateRawSnapshotResponse(
|
||||
_database.Cache,
|
||||
string.IsNullOrWhiteSpace(request.MessageId) ? Guid.NewGuid().ToString("N") : request.MessageId,
|
||||
request);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Detaches handlers from the server.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
_server.RemoveCultNetMessageListener<CultNetSnapshotRequestMessage>(_snapshotHandler);
|
||||
_server.RemoveCultNetMessageListener<CultNetDocumentPutRawMessage>(_putHandler);
|
||||
_server.RemoveCultNetMessageListener<CultNetDocumentDeleteMessage>(_deleteHandler);
|
||||
}
|
||||
|
||||
private Task HandleSnapshotRequestAsync(CultNetSnapshotRequestMessage request, NetPeer peer)
|
||||
{
|
||||
peer.SendCultNet(CreateSnapshotResponse(request));
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task HandlePutAsync(CultNetDocumentPutRawMessage message, NetPeer peer)
|
||||
{
|
||||
try
|
||||
{
|
||||
await ApplyPutAsync(message).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
peer.SendCultNet(new CultNetErrorMessage { Error = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleDeleteAsync(CultNetDocumentDeleteMessage message, NetPeer peer)
|
||||
{
|
||||
try
|
||||
{
|
||||
await ApplyDeleteAsync(message).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
peer.SendCultNet(new CultNetErrorMessage { Error = ex.Message });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -42,12 +42,18 @@ namespace GameCult.Networking
|
|||
/// </summary>
|
||||
public sealed class CultNetHost : IDisposable
|
||||
{
|
||||
internal CultNetHost(CultCache cache, SingleFileMessagePackBackingStore? store, Server server, CultNetDatabase database)
|
||||
internal CultNetHost(
|
||||
CultCache cache,
|
||||
SingleFileMessagePackBackingStore? store,
|
||||
Server server,
|
||||
CultNetDatabase database,
|
||||
CultNetDatabaseServer databaseServer)
|
||||
{
|
||||
Cache = cache;
|
||||
Store = store;
|
||||
Server = server;
|
||||
Database = database;
|
||||
DatabaseServer = databaseServer;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -70,6 +76,11 @@ namespace GameCult.Networking
|
|||
/// </summary>
|
||||
public CultNetDatabase Database { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the schema-v0 server bridge for distributed database messages.
|
||||
/// </summary>
|
||||
public CultNetDatabaseServer DatabaseServer { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Starts the server.
|
||||
/// </summary>
|
||||
|
|
@ -91,6 +102,7 @@ namespace GameCult.Networking
|
|||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
DatabaseServer.Dispose();
|
||||
Server.Dispose();
|
||||
Database.Dispose();
|
||||
Cache.Dispose();
|
||||
|
|
@ -118,9 +130,10 @@ namespace GameCult.Networking
|
|||
var server = new Server(cache, options.Security ?? ServerSecurityOptions.Development());
|
||||
var databaseOptions = options.DatabaseOptions ?? new CultNetDatabaseOptions();
|
||||
var database = new CultNetDatabase(cache, databaseOptions);
|
||||
var databaseServer = new CultNetDatabaseServer(server, database);
|
||||
options.ConfigureServer?.Invoke(server);
|
||||
|
||||
var host = new CultNetHost(cache, store, server, database);
|
||||
var host = new CultNetHost(cache, store, server, database, databaseServer);
|
||||
if (options.StartServer)
|
||||
{
|
||||
host.Start();
|
||||
|
|
|
|||
|
|
@ -57,6 +57,8 @@ Modern schema-v0 helpers live in:
|
|||
- `CultNetSchemaMessageSerialization`
|
||||
- `CultNetSchemaRegistry`
|
||||
- `CultNetDocumentRegistry`
|
||||
- `CultNetDatabase`
|
||||
- `CultNetDatabaseServer`
|
||||
- `NetPeerExtensions.SendCultNet(...)`
|
||||
|
||||
## Main Types
|
||||
|
|
@ -127,6 +129,11 @@ CultNet becomes the distribution model:
|
|||
- remote subscription fanout
|
||||
- reconnect resynchronization
|
||||
|
||||
The current first slice exposes this as `CultNetDatabase` and wires hosted
|
||||
servers through `CultNetDatabaseServer`. It handles primary-shard writes, typed
|
||||
R3 watch streams, raw document puts/deletes, and raw snapshot responses. Remote
|
||||
subscription messages and shard catalog exchange are the next missing organs.
|
||||
|
||||
The first coherent shard policy is primary ownership. Each shard has one
|
||||
authoritative writer at a time. Clients may connect to any node; a non-owner
|
||||
node forwards a write to the owner or rejects it honestly when forwarding is
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ namespace GameCult.Networking
|
|||
private const int MaxLoginAttemptsPerMinute = 5;
|
||||
|
||||
private readonly ConcurrentDictionary<Type, Delegate> _messageDelegates = new();
|
||||
private readonly ConcurrentDictionary<Type, Delegate> _cultNetMessageDelegates = new();
|
||||
private readonly ConcurrentDictionary<long, User> _users = new();
|
||||
private readonly ConcurrentDictionary<string, Queue<DateTimeOffset>> _connectionAttempts = new();
|
||||
private readonly ConcurrentDictionary<string, object> _connectionAttemptLocks = new();
|
||||
|
|
@ -92,6 +93,7 @@ namespace GameCult.Networking
|
|||
public void ClearMessageListeners()
|
||||
{
|
||||
_messageDelegates.Clear();
|
||||
_cultNetMessageDelegates.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -119,6 +121,49 @@ namespace GameCult.Networking
|
|||
AddMessageListener(callback);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a listener for a modern CultNet schema-v0 message type.
|
||||
/// </summary>
|
||||
public void AddCultNetMessageListener<T>(Func<T, NetPeer, Task> callback) where T : ICultNetSchemaMessage
|
||||
{
|
||||
var type = typeof(T);
|
||||
_cultNetMessageDelegates.AddOrUpdate(type,
|
||||
_ => callback,
|
||||
(t, current) =>
|
||||
{
|
||||
var combined = Delegate.Combine(current, callback) as Func<T, NetPeer, Task>;
|
||||
return combined ?? throw new InvalidOperationException($"Failed to combine delegates for {t.Name}");
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a listener for a modern CultNet schema-v0 message type.
|
||||
/// </summary>
|
||||
public void AddCultNetMessageListener<T>(Action<T, NetPeer> callback) where T : ICultNetSchemaMessage
|
||||
{
|
||||
AddCultNetMessageListener<T>((message, peer) =>
|
||||
{
|
||||
callback(message, peer);
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a listener for a modern CultNet schema-v0 message type.
|
||||
/// </summary>
|
||||
public void OnCultNet<T>(Func<T, NetPeer, Task> callback) where T : ICultNetSchemaMessage
|
||||
{
|
||||
AddCultNetMessageListener(callback);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a listener for a modern CultNet schema-v0 message type.
|
||||
/// </summary>
|
||||
public void OnCultNet<T>(Action<T, NetPeer> callback) where T : ICultNetSchemaMessage
|
||||
{
|
||||
AddCultNetMessageListener(callback);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a previously registered listener for a specific message type.
|
||||
/// </summary>
|
||||
|
|
@ -141,6 +186,25 @@ namespace GameCult.Networking
|
|||
RemoveMessageListener(callback);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a previously registered modern CultNet schema-v0 listener.
|
||||
/// </summary>
|
||||
public void RemoveCultNetMessageListener<T>(Delegate callback) where T : ICultNetSchemaMessage
|
||||
{
|
||||
if (_cultNetMessageDelegates.TryGetValue(typeof(T), out var currentDelegate))
|
||||
{
|
||||
var newDelegate = Delegate.Remove(currentDelegate, callback);
|
||||
if (newDelegate == null)
|
||||
{
|
||||
_cultNetMessageDelegates.TryRemove(typeof(T), out _);
|
||||
}
|
||||
else
|
||||
{
|
||||
_cultNetMessageDelegates[typeof(T)] = newDelegate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stops the underlying LiteNetLib server.
|
||||
/// </summary>
|
||||
|
|
@ -210,6 +274,14 @@ namespace GameCult.Networking
|
|||
try
|
||||
{
|
||||
var bytes = reader.GetRemainingBytes();
|
||||
var user = _users.GetOrAdd(peer.Id, _ => new User { Peer = peer });
|
||||
if (TryDeserializeCultNet(bytes, out var cultNetMessage))
|
||||
{
|
||||
Logger.LogDebug($"Received CultNet schema message {cultNetMessage.SchemaVersion}");
|
||||
await HandleCultNetSchemaMessageAsync(peer, user, cultNetMessage).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
var message = MessageSerialization.Deserialize<Message>(bytes);
|
||||
if (LogSensitivePayloads)
|
||||
{
|
||||
|
|
@ -225,8 +297,6 @@ namespace GameCult.Networking
|
|||
}
|
||||
|
||||
message.Peer = peer;
|
||||
var user = _users.GetOrAdd(peer.Id, _ => new User { Peer = peer });
|
||||
|
||||
if (message is LoginMessage or RegisterMessage or VerifyMessage)
|
||||
{
|
||||
if (IsVerified(user))
|
||||
|
|
@ -310,6 +380,53 @@ namespace GameCult.Networking
|
|||
Logger.LogInfo($"Server started on port {ServerPort}.");
|
||||
}
|
||||
|
||||
private async Task HandleCultNetSchemaMessageAsync(NetPeer peer, User user, ICultNetSchemaMessage message)
|
||||
{
|
||||
if (!IsVerified(user) && !CanProcessBeforeVerification(message))
|
||||
{
|
||||
peer.SendCultNet(new CultNetErrorMessage { Error = "User Not Verified" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (_cultNetMessageDelegates.TryGetValue(message.GetType(), out var del) && del != null)
|
||||
{
|
||||
foreach (var listener in del.GetInvocationList())
|
||||
{
|
||||
var result = listener.DynamicInvoke(message, peer);
|
||||
if (result is Task task)
|
||||
{
|
||||
await task.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
user.SessionExpiresAt = DateTimeOffset.UtcNow.AddSeconds(SessionTimeoutSeconds);
|
||||
RefreshSessionIfNeeded(peer, user);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogWarning($"No listener for CultNet schema message {message.SchemaVersion}");
|
||||
}
|
||||
}
|
||||
|
||||
private static bool CanProcessBeforeVerification(ICultNetSchemaMessage message)
|
||||
{
|
||||
return message is CultNetHelloMessage or CultNetSchemaCatalogRequestMessage;
|
||||
}
|
||||
|
||||
private static bool TryDeserializeCultNet(byte[] payload, out ICultNetSchemaMessage message)
|
||||
{
|
||||
try
|
||||
{
|
||||
message = CultNetSchemaMessageSerialization.Deserialize(payload);
|
||||
return true;
|
||||
}
|
||||
catch (MessagePackSerializationException)
|
||||
{
|
||||
message = null!;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -355,6 +355,40 @@ namespace GameCult.Networking.Tests
|
|||
Assert.That(changes[0].Document!.Username, Is.EqualTo("Raw"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task CultNetDatabaseServer_Creates_Filtered_SnapshotResponse()
|
||||
{
|
||||
var cache = new CultCache();
|
||||
var registry = new CultNetDocumentRegistry(cache.Registry)
|
||||
.Register(CultNetDocumentBinding.ForDocument<PlayerData>(
|
||||
cache.Registry,
|
||||
payloadSerializer: SerializePlayerDataPayload,
|
||||
payloadDeserializer: DeserializePlayerDataPayload));
|
||||
var database = new CultNetDatabase(cache, new CultNetDatabaseOptions
|
||||
{
|
||||
DocumentRegistry = registry
|
||||
});
|
||||
using var server = new Server(cache, DevelopmentServerSecurity);
|
||||
using var databaseServer = new CultNetDatabaseServer(server, database);
|
||||
var player = new PlayerData
|
||||
{
|
||||
PlayerId = Guid.NewGuid(),
|
||||
Email = "snapshot@example.test",
|
||||
PasswordHash = "hash",
|
||||
Username = "Snapshot"
|
||||
};
|
||||
var handle = await database.PutAsync(new CultRecordKey("player:snapshot"), player);
|
||||
|
||||
var response = databaseServer.CreateSnapshotResponse(registry.CreateSnapshotRequest(
|
||||
"snapshot-request",
|
||||
recordKeys: [handle.Key.Value]));
|
||||
|
||||
Assert.That(response.MessageId, Is.EqualTo("snapshot-request"));
|
||||
Assert.That(response.Documents, Has.Length.EqualTo(1));
|
||||
Assert.That(response.Documents[0].RecordKey, Is.EqualTo(handle.Key.Value));
|
||||
Assert.That(response.Documents[0].Payload, Is.EqualTo(SerializePlayerDataPayload(player)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CultNetSchemaRegistry_BuiltInCatalog_AdvertisesRawLane_AndSharedGhostlightContract()
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in a new issue