![]() |
VOOZH | about |
dotnet add package BeyondImmersion.Bannou.Client --version 2.0.0
NuGet\Install-Package BeyondImmersion.Bannou.Client -Version 2.0.0
<PackageReference Include="BeyondImmersion.Bannou.Client" Version="2.0.0" />
<PackageVersion Include="BeyondImmersion.Bannou.Client" Version="2.0.0" />Directory.Packages.props
<PackageReference Include="BeyondImmersion.Bannou.Client" />Project file
paket add BeyondImmersion.Bannou.Client --version 2.0.0
#r "nuget: BeyondImmersion.Bannou.Client, 2.0.0"
#:package BeyondImmersion.Bannou.Client@2.0.0
#addin nuget:?package=BeyondImmersion.Bannou.Client&version=2.0.0Install as a Cake Addin
#tool nuget:?package=BeyondImmersion.Bannou.Client&version=2.0.0Install as a Cake Tool
Lightweight SDK for game clients connecting to Bannou services via WebSocket.
The Client SDK provides everything a game client needs to communicate with Bannou:
client.Auth, client.Character, etc.)using BeyondImmersion.Bannou.Client;
// Create client and connect
var client = new BannouClient();
await client.ConnectWithTokenAsync(connectUrl, accessToken);
// Use typed service proxies (recommended)
var loginResponse = await client.Auth.LoginAsync(new LoginRequest
{
Email = "player@example.com",
Password = "secure-password"
});
if (loginResponse.IsSuccess)
{
Console.WriteLine($"Logged in as: {loginResponse.Result.AccountId}");
}
// Or use the generic InvokeAsync for custom endpoints
var response = await client.InvokeAsync<MyRequest, MyResponse>(
"POST",
"/character/get",
new MyRequest { CharacterId = "abc123" },
timeout: TimeSpan.FromSeconds(5));
// Handle typed events with disposable subscriptions
using var subscription = client.OnEvent<ChatMessageReceivedEvent>(evt =>
{
Console.WriteLine($"Chat from {evt.SenderId}: {evt.Message}");
});
// Or use the generic event handler for all events
client.OnEvent += (sender, eventData) =>
{
Console.WriteLine($"Event received: {eventData.EventName}");
};
// Clean up
await client.DisposeAsync();
var client = new BannouClient();
// Connect directly to an internal Connect node without JWT login
// Provide serviceToken when CONNECT_INTERNAL_AUTH_MODE=service-token; omit for network-trust.
await client.ConnectInternalAsync("ws://bannou-internal/connect", serviceToken: "shared-secret");
GameProtocolEnvelope (version + GameMessageType)LiteNetLibClientTransport (optional fuzz via TransportFuzzOptions)var transport = new LiteNetLibClientTransport();
await transport.ConnectAsync("127.0.0.1", 9000, GameProtocolEnvelope.CurrentVersion);
transport.OnServerMessage += (ver, type, payload) =>
{
if (type == GameMessageType.ArenaStateSnapshot)
{
var snap = MessagePackSerializer.Deserialize<ArenaStateSnapshot>(payload, GameProtocolEnvelope.DefaultOptions);
}
};
// Send input
var input = new PlayerInputMessage { Tick = 1, MoveX = 1, MoveY = 0 };
var bytes = MessagePackSerializer.Serialize(input, GameProtocolEnvelope.DefaultOptions);
await transport.SendAsync(GameMessageType.PlayerInput, bytes, reliable: true);
┌─────────────────┐ WebSocket ┌─────────────────┐
│ Game Client │◄──────────────────►│ Connect Service │
│ (Client SDK) │ Binary Protocol │ (Gateway) │
└─────────────────┘ └────────┬────────┘
│ mesh
┌────────┴────────┐
│ Bannou Services │
│ (Auth, Character│
│ GameSession..) │
└─────────────────┘
The Connect service is the WebSocket gateway. All client communication flows through it:
Messages use a hybrid format: binary header (31 bytes) + JSON payload.
┌─────────────────────────────────────────────────────────┐
│ Binary Header (31 bytes) │
├──────────┬─────────┬──────────┬──────────────┬──────────┤
│ Flags │ Channel │ Sequence │ Service GUID │ Msg ID │
│ (1 byte) │ (2) │ (4) │ (16) │ (8) │
├─────────────────────────────────────────────────────────┤
│ JSON Payload (variable length) │
│ { "characterId": "abc123", ... } │
└─────────────────────────────────────────────────────────┘
Why binary headers? Zero-copy routing. The Connect service extracts the 16-byte GUID and routes the message without parsing the JSON payload.
Why JSON payloads? Developer ergonomics. JSON is readable, debuggable, and works with any serializer.
When you connect, the server pushes a capability manifest - a list of APIs you can call:
{
"eventName": "connect.capability_manifest",
"sessionId": "abc123...",
"availableAPIs": [
{
"method": "POST",
"path": "/character/get",
"guid": "550e8400-e29b-41d4-a716-446655440000",
"service": "character"
}
]
}
The manifest updates dynamically as:
Use client.AvailableApis to see current capabilities:
foreach (var api in client.AvailableApis)
{
Console.WriteLine($"{api.Key}: {api.Value}");
}
Each client receives unique GUIDs for the same endpoints. This prevents security exploits where one client could use another client's GUIDs:
Client A: POST:/character/get → GUID abc123...
Client B: POST:/character/get → GUID xyz789... (different!)
The BannouClient handles this automatically - you just use method/path pairs:
// SDK looks up the correct GUID for your session
var response = await client.InvokeAsync<Req, Resp>("POST", "/character/get", request);
Shortcuts are pre-bound API calls pushed to clients. They encapsulate endpoint + parameters, allowing users to invoke complex operations with empty payloads.
Example flow:
SHORTCUT:join_game_arcadia// Wait for shortcut to appear
var shortcutGuid = client.GetServiceGuid("SHORTCUT", "join_game_arcadia");
if (shortcutGuid.HasValue)
{
// Invoke shortcut - server injects subscription/account data
var response = await client.InvokeAsync<object, JoinGameResponse>(
"SHORTCUT",
"join_game_arcadia",
new { }, // Empty payload - server fills in the rest
timeout: TimeSpan.FromSeconds(5));
}
All responses include a ResponseCode indicating success or failure:
| Code | Name | Description |
|---|---|---|
| 0 | OK | Request succeeded |
| 10 | RequestError | Malformed message |
| 11 | RequestTooLarge | Payload exceeds limit |
| 12 | TooManyRequests | Rate limited |
| 20 | Unauthorized | Auth required or invalid |
| 30 | ServiceNotFound | Target GUID not in manifest |
| 50-60 | Service_* | Backend service errors |
| 70-72 | Shortcut* | Shortcut expired/revoked |
See ResponseCodes.cs for the complete enumeration.
Instead of manually specifying HTTP methods and paths, use the generated typed proxies for compile-time safety:
// All services have typed proxies accessible as properties
var authResponse = await client.Auth.LoginAsync(new LoginRequest
{
Email = "player@example.com",
Password = "password"
});
var characterResponse = await client.Character.GetAsync(new CharacterGetRequest
{
CharacterId = characterId
});
// Available proxies include:
// client.Account, client.Auth, client.Character, client.GameSession,
// client.Matchmaking, client.Voice, client.Asset, and more...
The proxies handle GUID lookup, binary header construction, and response parsing automatically.
Subscribe to specific event types with full type safety:
// Subscribe to a specific event type - returns a disposable handle
using var chatSub = client.OnEvent<ChatMessageReceivedEvent>(evt =>
{
Console.WriteLine($"[{evt.SenderId}]: {evt.Message}");
});
using var matchSub = client.OnEvent<MatchFoundEvent>(evt =>
{
Console.WriteLine($"Match found! Players: {evt.PlayerCount}");
});
// Subscriptions are automatically cleaned up when disposed
// Or call subscription.Dispose() manually
For handling all events or custom event routing:
client.OnEvent += (sender, eventData) =>
{
switch (eventData.EventName)
{
case "game_session.player_joined":
var joinEvent = BannouJson.Deserialize<PlayerJoinedEvent>(eventData.Payload);
HandlePlayerJoined(joinEvent);
break;
case "connect.capability_manifest":
// Capabilities changed - new APIs available
break;
}
};
For better discoverability via IntelliSense, use service-grouped subscriptions:
// Organized by service - discoverable via client.Events.{Service}.On{Event}()
using var chatSub = client.Events.GameSession.OnChatMessageReceived(evt =>
{
Console.WriteLine($"[{evt.SenderId}]: {evt.Message}");
});
using var voiceSub = client.Events.Voice.OnVoicePeerJoined(evt =>
{
Console.WriteLine($"Peer joined: {evt.PeerId}");
});
using var matchSub = client.Events.Matchmaking.OnMatchFound(evt =>
{
ShowMatchUI(evt.MatchId, evt.PlayerCount);
});
The ClientEventRegistry maps between event types and their string names:
// Get the event name for a type
string? name = ClientEventRegistry.GetEventName<ChatMessageReceivedEvent>();
// Returns: "game_session.chat_received"
// Get the type for an event name
Type? type = ClientEventRegistry.GetEventType("matchmaking.match_found");
// Returns: typeof(MatchFoundEvent)
// Check if an event is registered
bool isRegistered = ClientEventRegistry.IsRegistered<VoicePeerJoinedEvent>();
The ClientEndpointMetadata class provides runtime lookup of request/response types:
// Get request type for an endpoint
var requestType = ClientEndpointMetadata.GetRequestType("POST", "/auth/login");
// Returns: typeof(LoginRequest)
// Get response type
var responseType = ClientEndpointMetadata.GetResponseType("POST", "/character/get");
// Returns: typeof(CharacterResponse)
// Get full endpoint info
var info = ClientEndpointMetadata.GetEndpointInfo("POST", "/auth/login");
// Returns: { Method, Path, Service, RequestType, ResponseType, Summary }
// Filter endpoints by service
var authEndpoints = ClientEndpointMetadata.GetEndpointsByService("Auth");
// Check if endpoint is registered
bool exists = ClientEndpointMetadata.IsRegistered("POST", "/account/get");
Request endpoint metadata instead of executing:
// Get JSON Schema for request body
var schema = await client.GetMetaAsync<JsonSchemaData>(
MetaType.RequestSchema,
"POST",
"/character/create");
// Get full endpoint documentation
var fullSchema = await client.GetMetaAsync<FullSchemaData>(
MetaType.FullSchema,
"POST",
"/character/create");
The edge-tester/ project demonstrates comprehensive Client SDK usage:
Key patterns from edge-tester:
// Create isolated test account
var accessToken = await CreateTestAccountAsync("test_prefix");
// Connect with token
var client = new BannouClient();
await client.ConnectWithTokenAsync(connectUrl, accessToken);
// Wait for capabilities
var deadline = DateTime.UtcNow.AddSeconds(10);
while (DateTime.UtcNow < deadline)
{
var guid = client.GetServiceGuid("POST", "/target/endpoint");
if (guid.HasValue) break;
await Task.Delay(500);
}
// Invoke and handle response
var response = await client.InvokeAsync<Request, Response>(
"POST", "/target/endpoint", request,
timeout: TimeSpan.FromSeconds(5));
if (response.IsSuccess)
{
// Process response.Result
}
else
{
// Handle response.Error (ResponseCode, Message, etc.)
}
| Component | Description |
|---|---|
BannouClient |
WebSocket client with connection management |
IBannouClient |
Interface for mocking in tests |
Generated/Proxies/* |
Typed service proxies (AuthProxy, CharacterProxy, etc.) |
Generated/Events/ClientEventRegistry |
Event type ↔ name mapping |
Generated/Events/ClientEndpointMetadata |
Runtime endpoint type discovery |
Generated/Events/*EventSubscriptions |
Service-grouped event subscriptions |
Generated/Events/BannouClientEvents |
Container for client.Events.{Service} access |
IEventSubscription |
Disposable event subscription handle |
BinaryMessage |
Binary protocol message handling |
ResponseCodes |
Error code enumeration |
MetaTypes |
Meta request/response types |
*Models.cs |
Generated request/response models |
*Events.cs |
Generated event types |
Behavior/Runtime/* |
ABML behavior interpreter |
The IBannouClient interface enables mocking for unit tests:
// In your game code, depend on the interface
public class GameManager
{
private readonly IBannouClient _client;
public GameManager(IBannouClient client)
{
_client = client;
}
}
// In tests, mock the client
var mockClient = new Mock<IBannouClient>();
mockClient.Setup(c => c.Auth.LoginAsync(It.IsAny<LoginRequest>(), ...))
.ReturnsAsync(new ApiResponse<AuthResponse> { IsSuccess = true, ... });
var gameManager = new GameManager(mockClient.Object);
Use BeyondImmersion.Bannou.Server instead if you need:
IAuthClient, IAccountClient, etc.)System.Net.WebSockets.Client - WebSocket connectivityThe SDK is intentionally lightweight for game client deployment.
dotnet add package BeyondImmersion.Bannou.Client
MIT License
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 net8.0 is compatible. net8.0-android net8.0-android was computed. net8.0-browser net8.0-browser was computed. net8.0-ios net8.0-ios was computed. net8.0-maccatalyst net8.0-maccatalyst was computed. net8.0-macos net8.0-macos was computed. net8.0-tvos net8.0-tvos was computed. net8.0-windows net8.0-windows was computed. net9.0 net9.0 is compatible. net9.0-android net9.0-android was computed. net9.0-browser net9.0-browser was computed. net9.0-ios net9.0-ios was computed. net9.0-maccatalyst net9.0-maccatalyst was computed. net9.0-macos net9.0-macos was computed. net9.0-tvos net9.0-tvos was computed. net9.0-windows net9.0-windows was computed. net10.0 net10.0 was computed. net10.0-android net10.0-android was computed. net10.0-browser net10.0-browser was computed. net10.0-ios net10.0-ios was computed. net10.0-maccatalyst net10.0-maccatalyst was computed. net10.0-macos net10.0-macos was computed. net10.0-tvos net10.0-tvos was computed. net10.0-windows net10.0-windows was computed. |
Showing the top 3 NuGet packages that depend on BeyondImmersion.Bannou.Client:
| Package | Downloads |
|---|---|
|
BeyondImmersion.Bannou.AssetBundler
Engine-agnostic asset bundling SDK for Bannou. Provides source adapters, processing pipeline, state tracking, and upload integration. |
|
|
BeyondImmersion.Bannou.Client.Voice
Voice/Audio helper for Bannou Client SDK. Provides P2P WebRTC voice chat using SIPSorcery. For game clients using Stride, Unity, or other C#/.NET game engines. |
|
|
BeyondImmersion.Bannou.AssetLoader.Client
WebSocket-based asset source for Bannou Asset Loader. Provides URL resolution for game clients and developer tools using the Bannou Client SDK. |
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 2.0.1-preview.manual... | 110 | 1/17/2026 |
| 2.0.1-preview.14 | 81 | 3/2/2026 |
| 2.0.1-preview.13 | 79 | 2/24/2026 |
| 2.0.1-preview.11 | 88 | 1/29/2026 |
| 2.0.1-preview.10 | 85 | 1/22/2026 |
| 2.0.1-preview.9 | 70 | 1/19/2026 |
| 2.0.1-preview.6 | 73 | 1/17/2026 |
| 2.0.1-preview.5 | 71 | 1/17/2026 |
| 2.0.0 | 250 | 1/17/2026 |
| 1.0.1-preview.4 | 76 | 1/17/2026 |
| 1.0.0 | 226 | 1/16/2026 |
| 0.1.0-preview.manual... | 79 | 1/16/2026 |
| 0.1.0-preview.544 | 93 | 1/14/2026 |
| 0.1.0-preview.3 | 78 | 1/16/2026 |