feat: freeze v1 transport contracts (#4)
quality-gate / quality (push) Successful in 51s

Closes #4
This commit is contained in:
KyuubiYoru
2026-07-16 04:52:38 +02:00
parent e626b89909
commit 69c8b2d2bc
43 changed files with 3737 additions and 12 deletions
@@ -39,7 +39,7 @@ public sealed class DependencyBoundaryTests
AssertDeclaredDependencies(
"src/FinalFactory.Rendezvous.Contracts/FinalFactory.Rendezvous.Contracts.csproj",
[],
[]);
["System.Text.Json"]);
AssertDeclaredDependencies(
"src/FinalFactory.Rendezvous.Client/FinalFactory.Rendezvous.Client.csproj",
["FinalFactory.Rendezvous.Contracts"],
@@ -47,7 +47,11 @@ public sealed class DependencyBoundaryTests
AssertDeclaredDependencies(
"src/FinalFactory.Rendezvous.Server/FinalFactory.Rendezvous.Server.csproj",
["FinalFactory.Rendezvous.Contracts"],
["LiteNetLib"]);
[
"LiteNetLib",
"Microsoft.AspNetCore.OpenApi",
"Microsoft.Extensions.ApiDescription.Server",
]);
AssertDeclaredDependencies(
"src/FinalFactory.Rendezvous.TestClient/FinalFactory.Rendezvous.TestClient.csproj",
["FinalFactory.Rendezvous.Client", "FinalFactory.Rendezvous.Contracts"],
@@ -0,0 +1,140 @@
using FinalFactory.Rendezvous.Contracts;
namespace FinalFactory.Rendezvous.Tests.Contracts;
public sealed class ContractLimitTests
{
[Fact]
public void ByteAndCollectionLimitsAcceptTheBoundaryOnly()
{
Assert.True(ContractValidation.IsHttpRequestSizeValid(ContractLimits.HttpRequestMaxBytes));
Assert.False(ContractValidation.IsHttpRequestSizeValid(ContractLimits.HttpRequestMaxBytes + 1));
Assert.True(ContractValidation.IsBrowserResponseSizeValid(ContractLimits.BrowserResponseMaxBytes));
Assert.False(ContractValidation.IsBrowserResponseSizeValid(ContractLimits.BrowserResponseMaxBytes + 1));
Assert.True(ContractValidation.IsPageSizeValid(ContractLimits.BrowserPageMaxItems));
Assert.False(ContractValidation.IsPageSizeValid(ContractLimits.BrowserPageMaxItems + 1));
Assert.False(ContractValidation.IsPageSizeValid(0));
}
[Fact]
public void Utf8LimitsCountBytesInsteadOfCharacters()
{
string atLimit = new('é', ContractLimits.DisplayNameMaxBytes / 2);
string overLimit = atLimit + "é";
Assert.True(ContractValidation.IsDisplayNameValid(atLimit));
Assert.False(ContractValidation.IsDisplayNameValid(overLimit));
Assert.True(ContractValidation.IsBuildVersionValid(
new string('a', ContractLimits.BuildVersionMaxBytes)));
Assert.False(ContractValidation.IsBuildVersionValid(
new string('a', ContractLimits.BuildVersionMaxBytes + 1)));
}
[Fact]
public void CapabilityLimitStaysBelowLiteNetLibTokenLimit()
{
Assert.True(ContractValidation.IsCapabilityValid(
new string('a', ContractLimits.UdpCapabilityMaxCharacters - 1)));
Assert.True(ContractValidation.IsCapabilityValid(
new string('a', ContractLimits.UdpCapabilityMaxCharacters)));
Assert.False(ContractValidation.IsCapabilityValid(
new string('a', ContractLimits.UdpCapabilityMaxCharacters + 1)));
Assert.False(ContractValidation.IsCapabilityValid("not+base64url"));
Assert.True(
ContractLimits.UdpCapabilityMaxCharacters
< ContractLimits.LiteNetLibNatTokenMaxCharacters);
}
[Fact]
public void CapacityAndMetadataLimitsAreBounded()
{
Assert.True(ContractValidation.IsCapacityValid(new SessionCapacity
{
CurrentPlayers = ContractLimits.SessionCapacityMaxPlayers,
MaximumPlayers = ContractLimits.SessionCapacityMaxPlayers,
}));
Assert.False(ContractValidation.IsCapacityValid(new SessionCapacity
{
CurrentPlayers = 0,
MaximumPlayers = ContractLimits.SessionCapacityMaxPlayers + 1,
}));
Dictionary<string, string> maximumKeys = Enumerable
.Range(0, ContractLimits.MetadataMaxKeys)
.ToDictionary(index => $"key-{index}", _ => "value", StringComparer.Ordinal);
Dictionary<string, string> tooManyKeys = new(maximumKeys, StringComparer.Ordinal)
{
["overflow"] = "value",
};
Assert.True(ContractValidation.IsMetadataValid(maximumKeys));
Assert.False(ContractValidation.IsMetadataValid(tooManyKeys));
Assert.False(ContractValidation.IsMetadataValid(new Dictionary<string, string>
{
[new string('k', ContractLimits.MetadataKeyMaxBytes + 1)] = "value",
}));
Assert.False(ContractValidation.IsMetadataValid(new Dictionary<string, string>
{
["key"] = new string('v', ContractLimits.MetadataValueMaxBytes + 1),
}));
}
[Fact]
public void MetadataDocumentLimitAcceptsExactlyFourKibibytes()
{
Dictionary<string, string> atLimit = Enumerable
.Range(0, ContractLimits.MetadataMaxKeys)
.ToDictionary(index => $"k{index:00}", _ => string.Empty, StringComparer.Ordinal);
for (int index = 0; index < 14; index++)
{
atLimit[$"k{index:00}"] = new string('v', ContractLimits.MetadataValueMaxBytes);
}
atLimit["k14"] = new string('v', 223);
Dictionary<string, string> overLimit = new(atLimit, StringComparer.Ordinal)
{
["k14"] = new string('v', 224),
};
Assert.True(ContractValidation.IsMetadataValid(atLimit));
Assert.False(ContractValidation.IsMetadataValid(overLimit));
}
[Fact]
public void EndpointValidationIsAddressFamilyAware()
{
Assert.True(ContractValidation.IsNetworkEndpointValid(new NetworkEndpoint
{
AddressFamily = AddressFamilyKind.Ipv4,
Address = "192.0.2.10",
Port = 9050,
}));
Assert.True(ContractValidation.IsNetworkEndpointValid(new NetworkEndpoint
{
AddressFamily = AddressFamilyKind.Ipv6,
Address = "2001:db8::10",
Port = 9050,
}));
Assert.False(ContractValidation.IsNetworkEndpointValid(new NetworkEndpoint
{
AddressFamily = AddressFamilyKind.Ipv4,
Address = "2001:db8::10",
Port = 9050,
}));
}
[Fact]
public void ScopeAndOpaqueTextBoundariesAreEnforced()
{
Assert.True(GameId.TryParse(new string('a', ContractLimits.GameIdMaxCharacters), out _));
Assert.False(GameId.TryParse(
new string('a', ContractLimits.GameIdMaxCharacters + 1),
out _));
Assert.True(ContractValidation.IsIdempotencyKeyValid(
new string('i', ContractLimits.IdempotencyKeyMaxCharacters)));
Assert.False(ContractValidation.IsIdempotencyKeyValid(
new string('i', ContractLimits.IdempotencyKeyMaxCharacters + 1)));
Assert.True(ContractValidation.IsCursorValid(null));
Assert.False(ContractValidation.IsCursorValid("contains whitespace"));
}
}
@@ -0,0 +1,104 @@
using System.Text.Json;
using FinalFactory.Rendezvous.Contracts;
namespace FinalFactory.Rendezvous.Tests.Contracts;
public sealed class ContractSerializationTests
{
public static TheoryData<string, Type> GoldenJsonVectors => new()
{
{ "register-session.json", typeof(RegisterSessionRequest) },
{ "browse-sessions.json", typeof(BrowseSessionsResponse) },
{ "create-join-response.json", typeof(CreateJoinAttemptResponse) },
{ "api-error.json", typeof(ApiError) },
};
[Theory]
[MemberData(nameof(GoldenJsonVectors))]
public void CanonicalJsonRoundtripsGoldenVectors(string fileName, Type contractType)
{
string expected = ContractTestFiles.Read(fileName);
object? value = JsonSerializer.Deserialize(expected, contractType, ContractJson.Options);
Assert.NotNull(value);
Assert.Equal(expected, JsonSerializer.Serialize(value, contractType, ContractJson.Options));
}
[Fact]
public void IdentifiersAreSerializedAsStrings()
{
SessionListingId id = new(new Guid("00112233-4455-6677-8899-aabbccddeeff"));
Assert.Equal(
"\"00112233-4455-6677-8899-aabbccddeeff\"",
JsonSerializer.Serialize(id, ContractJson.Options));
Assert.Equal(id, JsonSerializer.Deserialize<SessionListingId>(
"\"00112233-4455-6677-8899-aabbccddeeff\"",
ContractJson.Options));
}
[Fact]
public void UnknownObjectFieldsAreIgnoredForAdditiveV1Changes()
{
const string json = """
{"contractVersion":1,"idempotencyKey":"join-001","gameId":"space-game","environmentId":"production","listingId":"00112233-4455-6677-8899-aabbccddeeff","protocolVersion":7,"futureField":{"nested":true}}
""";
CreateJoinAttemptRequest? request = JsonSerializer.Deserialize<CreateJoinAttemptRequest>(
json,
ContractJson.Options);
Assert.NotNull(request);
Assert.Equal(new GameId("space-game"), request.GameId);
Assert.Equal(7u, request.ProtocolVersion);
}
[Fact]
public void MissingNormativeFieldsAreRejectedInsteadOfDefaulted()
{
const string missingVersion = """
{"idempotencyKey":"join-001","gameId":"space-game","environmentId":"production","listingId":"00112233-4455-6677-8899-aabbccddeeff","protocolVersion":7}
""";
Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<CreateJoinAttemptRequest>(
missingVersion,
ContractJson.Options));
}
[Fact]
public void UnknownEnumNamesAndNumericValuesAreRejected()
{
Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<ReportConnectionOutcomeRequest>(
"{\"contractVersion\":1,\"outcome\":\"futureOutcome\",\"elapsedMilliseconds\":1}",
ContractJson.Options));
Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<ReportConnectionOutcomeRequest>(
"{\"contractVersion\":1,\"outcome\":99,\"elapsedMilliseconds\":1}",
ContractJson.Options));
}
[Theory]
[InlineData(0)]
[InlineData(2)]
[InlineData(int.MaxValue)]
public void UnknownContractVersionsFailPredictably(int version)
{
Assert.Equal(
RendezvousErrorCode.UnsupportedContractVersion,
ContractValidation.ValidateContractVersion(version));
}
[Fact]
public void GameplayProtocolCompatibilityIsExactAndBuildIndependent()
{
Assert.True(ContractValidation.AreProtocolsCompatible(7, 7));
Assert.False(ContractValidation.AreProtocolsCompatible(7, 8));
}
[Fact]
public void SharedCanonicalOptionsCannotBeMutatedByConsumers()
{
Assert.True(ContractJson.Options.IsReadOnly);
Assert.Throws<InvalidOperationException>(() =>
ContractJson.Options.WriteIndented = true);
}
}
@@ -0,0 +1,30 @@
namespace FinalFactory.Rendezvous.Tests.Contracts;
internal static class ContractTestFiles
{
public static string Read(string fileName) => File
.ReadAllText(Path.Combine(Directory, fileName))
.TrimEnd('\r', '\n');
public static string Directory
{
get
{
DirectoryInfo? directory = new(AppContext.BaseDirectory);
while (directory is not null)
{
string solution = Path.Combine(directory.FullName, "Rendezvous.slnx");
if (File.Exists(solution))
{
return Path.Combine(
directory.FullName,
"tests/FinalFactory.Rendezvous.Tests/TestData/Contracts/v1");
}
directory = directory.Parent;
}
throw new DirectoryNotFoundException("Could not locate contract test data.");
}
}
}
@@ -0,0 +1,68 @@
using System.Text.Json;
namespace FinalFactory.Rendezvous.Tests.Contracts;
public sealed class OpenApiCompatibilityTests
{
private static readonly string[] ExpectedPaths =
[
"/health/live",
"/health/ready",
"/v1/join-attempts",
"/v1/join-attempts/{attemptId}/outcome",
"/v1/sessions",
"/v1/sessions/{listingId}",
"/v1/sessions/{listingId}/join-attempts",
"/v1/sessions/{listingId}/renew",
];
private static readonly string[] ExpectedListingProperties =
[
"buildVersion",
"capacity",
"contractVersion",
"displayName",
"environmentId",
"gameId",
"listingId",
"metadata",
"protocolVersion",
"publisherTrustMode",
"regionId",
"visibility",
];
[Fact]
public void GeneratedOpenApiContainsTheFrozenV1Surface()
{
string path = Path.Combine(
ContractTestFiles.Directory,
"../../../../../docs/api/rendezvous-v1.json");
using JsonDocument document = JsonDocument.Parse(File.ReadAllText(Path.GetFullPath(path)));
JsonElement root = document.RootElement;
Assert.Equal("3.1.1", root.GetProperty("openapi").GetString());
string[] paths = root.GetProperty("paths")
.EnumerateObject()
.Select(static item => item.Name)
.Order(StringComparer.Ordinal)
.ToArray();
Assert.Equal(ExpectedPaths, paths);
JsonElement schemas = root.GetProperty("components").GetProperty("schemas");
Assert.Equal("string", schemas.GetProperty("GameId").GetProperty("type").GetString());
Assert.Equal("uuid", schemas.GetProperty("SessionListingId").GetProperty("format").GetString());
string[] listingProperties = schemas.GetProperty("SessionListing")
.GetProperty("properties")
.EnumerateObject()
.Select(static item => item.Name)
.Order(StringComparer.Ordinal)
.ToArray();
Assert.Equal(ExpectedListingProperties, listingProperties);
Assert.DoesNotContain(listingProperties, static property =>
property.Contains("token", StringComparison.OrdinalIgnoreCase)
|| property.Contains("endpoint", StringComparison.OrdinalIgnoreCase)
|| property.Contains("playerId", StringComparison.OrdinalIgnoreCase));
}
}
@@ -0,0 +1,103 @@
using System.Reflection;
using FinalFactory.Rendezvous.Contracts;
namespace FinalFactory.Rendezvous.Tests.Contracts;
public sealed class PublicApiCompatibilityTests
{
[Fact]
public void ContractsPublicApiMatchesTheV1Snapshot()
{
string snapshot = CreateSnapshot(typeof(ContractLimits).Assembly);
string expected = ContractTestFiles.Read("contracts-public-api.txt");
if (expected == "SNAPSHOT_PENDING"
&& Environment.GetEnvironmentVariable("RENDEZVOUS_UPDATE_CONTRACT_SNAPSHOT") == "1")
{
string snapshotPath = Path.Combine(
ContractTestFiles.Directory,
"contracts-public-api.txt");
File.WriteAllText(snapshotPath, snapshot + Environment.NewLine);
expected = snapshot;
}
Assert.Equal(expected, snapshot);
}
private static string CreateSnapshot(Assembly assembly)
{
List<string> lines = [];
foreach (Type type in assembly.GetExportedTypes().OrderBy(static type => type.FullName, StringComparer.Ordinal))
{
lines.Add($"TYPE {FormatType(type)}");
if (type.IsEnum)
{
foreach (string name in Enum.GetNames(type))
{
object value = Enum.Parse(type, name);
lines.Add($" ENUM {name}={Convert.ToInt64(value, System.Globalization.CultureInfo.InvariantCulture)}");
}
continue;
}
foreach (FieldInfo field in type.GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly)
.OrderBy(static field => field.Name, StringComparer.Ordinal))
{
string value = field.IsLiteral
? Convert.ToString(field.GetRawConstantValue(), System.Globalization.CultureInfo.InvariantCulture) ?? "null"
: "non-literal";
lines.Add($" FIELD {FormatType(field.FieldType)} {field.Name}={value}");
}
foreach (ConstructorInfo constructor in type.GetConstructors(BindingFlags.Public | BindingFlags.Instance)
.OrderBy(static constructor => FormatParameters(constructor.GetParameters()), StringComparer.Ordinal))
{
lines.Add($" CTOR ({FormatParameters(constructor.GetParameters())})");
}
foreach (PropertyInfo property in type.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly)
.OrderBy(static property => property.Name, StringComparer.Ordinal))
{
string accessors = $"{(property.CanRead ? "get;" : string.Empty)}{(property.CanWrite ? "set;" : string.Empty)}";
lines.Add($" PROP {FormatType(property.PropertyType)} {property.Name} {{{accessors}}}");
}
foreach (MethodInfo method in type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly)
.Where(static method => !method.IsSpecialName || method.Name.StartsWith("op_", StringComparison.Ordinal))
.OrderBy(static method => method.Name, StringComparer.Ordinal)
.ThenBy(static method => FormatParameters(method.GetParameters()), StringComparer.Ordinal))
{
lines.Add($" METHOD {FormatType(method.ReturnType)} {method.Name}({FormatParameters(method.GetParameters())})");
}
}
return string.Join('\n', lines);
}
private static string FormatParameters(ParameterInfo[] parameters) => string.Join(
", ",
parameters.Select(static parameter =>
$"{FormatType(parameter.ParameterType)} {parameter.Name}"));
private static string FormatType(Type type)
{
if (type.IsByRef)
{
return $"{FormatType(type.GetElementType()!)}&";
}
if (type.IsArray)
{
return $"{FormatType(type.GetElementType()!)}[]";
}
if (type.IsGenericType)
{
string name = type.GetGenericTypeDefinition().FullName!;
name = name[..name.IndexOf('`')];
return $"{name}<{string.Join(",", type.GetGenericArguments().Select(FormatType))}>";
}
return type.FullName ?? type.Name;
}
}
@@ -0,0 +1,101 @@
using FinalFactory.Rendezvous.Contracts;
namespace FinalFactory.Rendezvous.Tests.Contracts;
public sealed class UdpCodecTests
{
private static readonly Guid Handle = new("00112233-4455-6677-8899-aabbccddeeff");
[Fact]
public void HostPresenceMatchesTheV1GoldenVector()
{
byte[] encoded = RendezvousUdpCodec.Encode(CreateDatagram());
Assert.Equal(ContractTestFiles.Read("udp-host-ipv4.hex"), Convert.ToHexString(encoded).ToLowerInvariant());
Assert.True(RendezvousUdpCodec.TryDecode(encoded, out PresenceDatagram? decoded, out UdpDecodeError error));
Assert.Equal(UdpDecodeError.None, error);
Assert.NotNull(decoded);
Assert.Equal(Handle, decoded.MediationHandle.Value);
Assert.Equal("192.0.2.10", decoded.LocalAddress);
Assert.Equal(9050, decoded.LocalPort);
Assert.Equal("Abc_123-xYz", decoded.Capability);
}
[Fact]
public void Ipv6RoundtripsWithoutLosingItsAddressFamily()
{
PresenceDatagram original = CreateDatagram();
original.MessageType = UdpPresenceMessageType.ClientPresence;
original.AddressFamily = AddressFamilyKind.Ipv6;
original.LocalAddress = "2001:db8::10";
byte[] encoded = RendezvousUdpCodec.Encode(original);
Assert.True(RendezvousUdpCodec.TryDecode(encoded, out PresenceDatagram? decoded, out _));
Assert.NotNull(decoded);
Assert.Equal(AddressFamilyKind.Ipv6, decoded.AddressFamily);
Assert.Equal("2001:db8::10", decoded.LocalAddress);
}
[Fact]
public void EncoderRejectsAnAddressFamilyMismatch()
{
PresenceDatagram datagram = CreateDatagram();
datagram.AddressFamily = AddressFamilyKind.Ipv4;
datagram.LocalAddress = "2001:db8::10";
Assert.Throws<ArgumentException>(() => RendezvousUdpCodec.Encode(datagram));
}
[Theory]
[InlineData(2, 2, UdpDecodeError.UnsupportedVersion)]
[InlineData(3, 3, UdpDecodeError.UnknownMessageType)]
[InlineData(4, 1, UdpDecodeError.InvalidFlags)]
[InlineData(21, 5, UdpDecodeError.InvalidAddressFamily)]
public void DecoderReturnsStableErrorsForUnknownHeaderValues(
int offset,
byte replacement,
UdpDecodeError expected)
{
byte[] encoded = RendezvousUdpCodec.Encode(CreateDatagram());
encoded[offset] = replacement;
Assert.False(RendezvousUdpCodec.TryDecode(encoded, out _, out UdpDecodeError actual));
Assert.Equal(expected, actual);
}
[Fact]
public void DecoderRejectsTruncationTrailingDataAndOversizedPackets()
{
byte[] encoded = RendezvousUdpCodec.Encode(CreateDatagram());
byte[] trailing = [.. encoded, 0];
byte[] oversized = new byte[ContractLimits.UdpDatagramMaxBytes + 1];
Assert.False(RendezvousUdpCodec.TryDecode(encoded.AsSpan(0, encoded.Length - 1), out _, out UdpDecodeError truncated));
Assert.Equal(UdpDecodeError.Truncated, truncated);
Assert.False(RendezvousUdpCodec.TryDecode(trailing, out _, out UdpDecodeError trailingError));
Assert.Equal(UdpDecodeError.TrailingData, trailingError);
Assert.False(RendezvousUdpCodec.TryDecode(oversized, out _, out UdpDecodeError oversizedError));
Assert.Equal(UdpDecodeError.DatagramTooLarge, oversizedError);
}
[Fact]
public void DecoderRejectsNonBase64UrlCapabilities()
{
byte[] encoded = RendezvousUdpCodec.Encode(CreateDatagram());
encoded[^1] = (byte)'+';
Assert.False(RendezvousUdpCodec.TryDecode(encoded, out _, out UdpDecodeError error));
Assert.Equal(UdpDecodeError.InvalidCapability, error);
}
private static PresenceDatagram CreateDatagram() => new()
{
MessageType = UdpPresenceMessageType.HostPresence,
MediationHandle = new MediationHandle(Handle),
AddressFamily = AddressFamilyKind.Ipv4,
LocalAddress = "192.0.2.10",
LocalPort = 9050,
Capability = "Abc_123-xYz",
};
}
@@ -0,0 +1 @@
{"contractVersion":1,"code":"rateLimited","message":"Try again later.","correlationId":"request-001","retryAfterSeconds":3}
@@ -0,0 +1 @@
{"contractVersion":1,"items":[{"contractVersion":1,"listingId":"00112233-4455-6677-8899-aabbccddeeff","gameId":"space-game","environmentId":"production","regionId":"eu-central","protocolVersion":7,"buildVersion":"1.4.2","displayName":"Europa Relay","visibility":"public","publisherTrustMode":"managedDedicated","capacity":{"currentPlayers":2,"maximumPlayers":8},"metadata":{"mode":"co-op","map":"europa"}}],"nextCursor":"cursor-002"}
@@ -0,0 +1,315 @@
TYPE FinalFactory.Rendezvous.Contracts.AddressFamilyKind
ENUM Ipv4=4
ENUM Ipv6=6
TYPE FinalFactory.Rendezvous.Contracts.ApiError
CTOR ()
PROP FinalFactory.Rendezvous.Contracts.RendezvousErrorCode Code {get;set;}
PROP System.Int32 ContractVersion {get;set;}
PROP System.String CorrelationId {get;set;}
PROP System.String Message {get;set;}
PROP System.Nullable<System.Int32> RetryAfterSeconds {get;set;}
TYPE FinalFactory.Rendezvous.Contracts.BrowseHostJoinAttemptsResponse
CTOR ()
PROP System.Int32 ContractVersion {get;set;}
PROP System.Collections.Generic.List<FinalFactory.Rendezvous.Contracts.HostJoinAttempt> Items {get;set;}
PROP System.String NextCursor {get;set;}
TYPE FinalFactory.Rendezvous.Contracts.BrowseSessionsRequest
CTOR ()
PROP System.Int32 ContractVersion {get;set;}
PROP System.String Cursor {get;set;}
PROP FinalFactory.Rendezvous.Contracts.EnvironmentId EnvironmentId {get;set;}
PROP FinalFactory.Rendezvous.Contracts.GameId GameId {get;set;}
PROP System.Int32 PageSize {get;set;}
PROP System.UInt32 ProtocolVersion {get;set;}
PROP System.Nullable<FinalFactory.Rendezvous.Contracts.RegionId> RegionId {get;set;}
TYPE FinalFactory.Rendezvous.Contracts.BrowseSessionsResponse
CTOR ()
PROP System.Int32 ContractVersion {get;set;}
PROP System.Collections.Generic.List<FinalFactory.Rendezvous.Contracts.SessionListing> Items {get;set;}
PROP System.String NextCursor {get;set;}
TYPE FinalFactory.Rendezvous.Contracts.ConnectionOutcomeKind
ENUM Connected=1
ENUM Cancelled=2
ENUM TimedOut=3
ENUM IncompatibleProtocol=4
ENUM StaleHost=5
ENUM ServiceRejected=6
ENUM HostRejected=7
ENUM TransportFailed=8
ENUM FallbackOffered=9
TYPE FinalFactory.Rendezvous.Contracts.ContractJson
PROP System.Text.Json.JsonSerializerOptions Options {get;}
METHOD System.Void Configure(System.Text.Json.JsonSerializerOptions options)
METHOD System.Text.Json.JsonSerializerOptions CreateOptions()
TYPE FinalFactory.Rendezvous.Contracts.ContractLimits
FIELD System.Int32 BrowserPageMaxItems=100
FIELD System.Int32 BrowserResponseMaxBytes=262144
FIELD System.Int32 BuildVersionMaxBytes=64
FIELD System.Int32 ConnectionTicketMaxCharacters=192
FIELD System.Int32 ContractVersion=1
FIELD System.Int32 CursorMaxCharacters=512
FIELD System.Int32 DiagnosticCodeMaxCharacters=64
FIELD System.Int32 DisplayNameMaxBytes=128
FIELD System.Int32 EnvironmentIdMaxCharacters=32
FIELD System.Int32 ErrorMessageMaxBytes=256
FIELD System.Int32 GameIdMaxCharacters=64
FIELD System.Int32 HttpRequestMaxBytes=16384
FIELD System.Int32 IdempotencyKeyMaxCharacters=64
FIELD System.Int32 LiteNetLibNatTokenMaxCharacters=256
FIELD System.Int32 MetadataKeyMaxBytes=64
FIELD System.Int32 MetadataMaxBytes=4096
FIELD System.Int32 MetadataMaxKeys=32
FIELD System.Int32 MetadataValueMaxBytes=256
FIELD System.Int32 OpaqueHttpCredentialMaxCharacters=1024
FIELD System.Int32 RegionIdMaxCharacters=32
FIELD System.Int32 SessionCapacityMaxPlayers=10000
FIELD System.Int32 UdpCapabilityMaxCharacters=192
FIELD System.Int32 UdpDatagramMaxBytes=1200
TYPE FinalFactory.Rendezvous.Contracts.ContractValidation
METHOD System.Boolean AreProtocolsCompatible(System.UInt32 requested, System.UInt32 offered)
METHOD System.Boolean IsBrowserResponseSizeValid(System.Int32 byteCount)
METHOD System.Boolean IsBuildVersionValid(System.String value)
METHOD System.Boolean IsCapabilityValid(System.String capability)
METHOD System.Boolean IsCapacityValid(FinalFactory.Rendezvous.Contracts.SessionCapacity capacity)
METHOD System.Boolean IsConnectionTicketValid(System.String ticket)
METHOD System.Boolean IsCursorValid(System.String value)
METHOD System.Boolean IsDiagnosticCodeValid(System.String value)
METHOD System.Boolean IsDisplayNameValid(System.String value)
METHOD System.Boolean IsHttpRequestSizeValid(System.Int32 byteCount)
METHOD System.Boolean IsIdempotencyKeyValid(System.String value)
METHOD System.Boolean IsMetadataValid(System.Collections.Generic.IReadOnlyDictionary<System.String,System.String> metadata)
METHOD System.Boolean IsNetworkEndpointValid(FinalFactory.Rendezvous.Contracts.NetworkEndpoint endpoint)
METHOD System.Boolean IsOpaqueHttpCredentialValid(System.String value)
METHOD System.Boolean IsPageSizeValid(System.Int32 pageSize)
METHOD System.Boolean IsUtf8LengthWithin(System.String value, System.Int32 maximumBytes)
METHOD FinalFactory.Rendezvous.Contracts.RendezvousErrorCode ValidateContractVersion(System.Int32 contractVersion)
TYPE FinalFactory.Rendezvous.Contracts.CreateJoinAttemptRequest
CTOR ()
PROP System.Int32 ContractVersion {get;set;}
PROP FinalFactory.Rendezvous.Contracts.EnvironmentId EnvironmentId {get;set;}
PROP FinalFactory.Rendezvous.Contracts.GameId GameId {get;set;}
PROP System.String IdempotencyKey {get;set;}
PROP FinalFactory.Rendezvous.Contracts.SessionListingId ListingId {get;set;}
PROP System.UInt32 ProtocolVersion {get;set;}
TYPE FinalFactory.Rendezvous.Contracts.CreateJoinAttemptResponse
CTOR ()
PROP FinalFactory.Rendezvous.Contracts.JoinAttemptId AttemptId {get;set;}
PROP System.String ClientPunchCapability {get;set;}
PROP System.Int32 ContractVersion {get;set;}
PROP FinalFactory.Rendezvous.Contracts.NetworkEndpoint DedicatedFallback {get;set;}
PROP System.DateTimeOffset ExpiresAt {get;set;}
PROP FinalFactory.Rendezvous.Contracts.MediationHandle MediationHandle {get;set;}
TYPE FinalFactory.Rendezvous.Contracts.DeleteSessionRequest
CTOR ()
PROP System.Int32 ContractVersion {get;set;}
PROP System.String LeaseToken {get;set;}
TYPE FinalFactory.Rendezvous.Contracts.EnvironmentId
CTOR (System.String value)
PROP System.String Value {get;}
METHOD System.Boolean Equals(FinalFactory.Rendezvous.Contracts.EnvironmentId other)
METHOD System.Boolean Equals(System.Object obj)
METHOD System.Int32 GetHashCode()
METHOD System.String ToString()
METHOD System.Boolean TryParse(System.String value, FinalFactory.Rendezvous.Contracts.EnvironmentId& environmentId)
METHOD System.Boolean op_Equality(FinalFactory.Rendezvous.Contracts.EnvironmentId left, FinalFactory.Rendezvous.Contracts.EnvironmentId right)
METHOD System.Boolean op_Inequality(FinalFactory.Rendezvous.Contracts.EnvironmentId left, FinalFactory.Rendezvous.Contracts.EnvironmentId right)
TYPE FinalFactory.Rendezvous.Contracts.GameId
CTOR (System.String value)
PROP System.String Value {get;}
METHOD System.Boolean Equals(FinalFactory.Rendezvous.Contracts.GameId other)
METHOD System.Boolean Equals(System.Object obj)
METHOD System.Int32 GetHashCode()
METHOD System.String ToString()
METHOD System.Boolean TryParse(System.String value, FinalFactory.Rendezvous.Contracts.GameId& gameId)
METHOD System.Boolean op_Equality(FinalFactory.Rendezvous.Contracts.GameId left, FinalFactory.Rendezvous.Contracts.GameId right)
METHOD System.Boolean op_Inequality(FinalFactory.Rendezvous.Contracts.GameId left, FinalFactory.Rendezvous.Contracts.GameId right)
TYPE FinalFactory.Rendezvous.Contracts.GetSessionResponse
CTOR ()
PROP System.Int32 ContractVersion {get;set;}
PROP FinalFactory.Rendezvous.Contracts.SessionListing Session {get;set;}
TYPE FinalFactory.Rendezvous.Contracts.HealthResponse
CTOR ()
PROP System.Int32 ContractVersion {get;set;}
PROP System.String Status {get;set;}
TYPE FinalFactory.Rendezvous.Contracts.HostJoinAttempt
CTOR ()
PROP FinalFactory.Rendezvous.Contracts.JoinAttemptId AttemptId {get;set;}
PROP System.DateTimeOffset ExpiresAt {get;set;}
PROP System.String HostPunchCapability {get;set;}
PROP FinalFactory.Rendezvous.Contracts.MediationHandle MediationHandle {get;set;}
TYPE FinalFactory.Rendezvous.Contracts.JoinAttemptId
CTOR (System.Guid value)
PROP System.Guid Value {get;}
METHOD System.Boolean Equals(FinalFactory.Rendezvous.Contracts.JoinAttemptId other)
METHOD System.Boolean Equals(System.Object obj)
METHOD System.Int32 GetHashCode()
METHOD System.String ToString()
METHOD System.Boolean TryParse(System.String value, FinalFactory.Rendezvous.Contracts.JoinAttemptId& id)
METHOD System.Boolean op_Equality(FinalFactory.Rendezvous.Contracts.JoinAttemptId left, FinalFactory.Rendezvous.Contracts.JoinAttemptId right)
METHOD System.Boolean op_Inequality(FinalFactory.Rendezvous.Contracts.JoinAttemptId left, FinalFactory.Rendezvous.Contracts.JoinAttemptId right)
TYPE FinalFactory.Rendezvous.Contracts.LeaseId
CTOR (System.Guid value)
PROP System.Guid Value {get;}
METHOD System.Boolean Equals(FinalFactory.Rendezvous.Contracts.LeaseId other)
METHOD System.Boolean Equals(System.Object obj)
METHOD System.Int32 GetHashCode()
METHOD System.String ToString()
METHOD System.Boolean TryParse(System.String value, FinalFactory.Rendezvous.Contracts.LeaseId& id)
METHOD System.Boolean op_Equality(FinalFactory.Rendezvous.Contracts.LeaseId left, FinalFactory.Rendezvous.Contracts.LeaseId right)
METHOD System.Boolean op_Inequality(FinalFactory.Rendezvous.Contracts.LeaseId left, FinalFactory.Rendezvous.Contracts.LeaseId right)
TYPE FinalFactory.Rendezvous.Contracts.ListingVisibility
ENUM Public=1
ENUM Unlisted=2
TYPE FinalFactory.Rendezvous.Contracts.MediationHandle
CTOR (System.Guid value)
PROP System.Guid Value {get;}
METHOD System.Boolean Equals(FinalFactory.Rendezvous.Contracts.MediationHandle other)
METHOD System.Boolean Equals(System.Object obj)
METHOD System.Int32 GetHashCode()
METHOD System.String ToString()
METHOD System.Boolean TryParse(System.String value, FinalFactory.Rendezvous.Contracts.MediationHandle& id)
METHOD System.Boolean op_Equality(FinalFactory.Rendezvous.Contracts.MediationHandle left, FinalFactory.Rendezvous.Contracts.MediationHandle right)
METHOD System.Boolean op_Inequality(FinalFactory.Rendezvous.Contracts.MediationHandle left, FinalFactory.Rendezvous.Contracts.MediationHandle right)
TYPE FinalFactory.Rendezvous.Contracts.NetworkEndpoint
CTOR ()
PROP System.String Address {get;set;}
PROP FinalFactory.Rendezvous.Contracts.AddressFamilyKind AddressFamily {get;set;}
PROP System.Int32 Port {get;set;}
TYPE FinalFactory.Rendezvous.Contracts.PresenceDatagram
CTOR ()
PROP FinalFactory.Rendezvous.Contracts.AddressFamilyKind AddressFamily {get;set;}
PROP System.String Capability {get;set;}
PROP System.Int32 ContractVersion {get;set;}
PROP System.String LocalAddress {get;set;}
PROP System.Int32 LocalPort {get;set;}
PROP FinalFactory.Rendezvous.Contracts.MediationHandle MediationHandle {get;set;}
PROP FinalFactory.Rendezvous.Contracts.UdpPresenceMessageType MessageType {get;set;}
TYPE FinalFactory.Rendezvous.Contracts.PublisherTrustMode
ENUM ManagedDedicated=1
ENUM PlayerGrant=2
ENUM AnonymousUnlisted=3
TYPE FinalFactory.Rendezvous.Contracts.RegionId
CTOR (System.String value)
PROP System.String Value {get;}
METHOD System.Boolean Equals(FinalFactory.Rendezvous.Contracts.RegionId other)
METHOD System.Boolean Equals(System.Object obj)
METHOD System.Int32 GetHashCode()
METHOD System.String ToString()
METHOD System.Boolean TryParse(System.String value, FinalFactory.Rendezvous.Contracts.RegionId& regionId)
METHOD System.Boolean op_Equality(FinalFactory.Rendezvous.Contracts.RegionId left, FinalFactory.Rendezvous.Contracts.RegionId right)
METHOD System.Boolean op_Inequality(FinalFactory.Rendezvous.Contracts.RegionId left, FinalFactory.Rendezvous.Contracts.RegionId right)
TYPE FinalFactory.Rendezvous.Contracts.RegisterSessionRequest
CTOR ()
PROP System.String BuildVersion {get;set;}
PROP FinalFactory.Rendezvous.Contracts.SessionCapacity Capacity {get;set;}
PROP System.Int32 ContractVersion {get;set;}
PROP System.String DisplayName {get;set;}
PROP FinalFactory.Rendezvous.Contracts.EnvironmentId EnvironmentId {get;set;}
PROP FinalFactory.Rendezvous.Contracts.GameId GameId {get;set;}
PROP System.String IdempotencyKey {get;set;}
PROP System.Collections.Generic.Dictionary<System.String,System.String> Metadata {get;set;}
PROP System.UInt32 ProtocolVersion {get;set;}
PROP FinalFactory.Rendezvous.Contracts.RegionId RegionId {get;set;}
PROP FinalFactory.Rendezvous.Contracts.ListingVisibility Visibility {get;set;}
TYPE FinalFactory.Rendezvous.Contracts.RegisterSessionResponse
CTOR ()
PROP System.Int32 ContractVersion {get;set;}
PROP System.DateTimeOffset ExpiresAt {get;set;}
PROP System.String HostPresenceCapability {get;set;}
PROP FinalFactory.Rendezvous.Contracts.MediationHandle HostPresenceHandle {get;set;}
PROP FinalFactory.Rendezvous.Contracts.LeaseId LeaseId {get;set;}
PROP System.String LeaseToken {get;set;}
PROP FinalFactory.Rendezvous.Contracts.SessionListingId ListingId {get;set;}
TYPE FinalFactory.Rendezvous.Contracts.RendezvousErrorCode
ENUM None=0
ENUM InvalidRequest=1
ENUM UnsupportedContractVersion=2
ENUM IncompatibleProtocol=3
ENUM AuthenticationRequired=4
ENUM Forbidden=5
ENUM NotFound=6
ENUM Conflict=7
ENUM RateLimited=8
ENUM StaleHost=9
ENUM Expired=10
ENUM ReplayRejected=11
ENUM CapacityExceeded=12
ENUM ServiceUnavailable=13
ENUM InternalError=14
TYPE FinalFactory.Rendezvous.Contracts.RendezvousUdpCodec
FIELD System.Byte FlagsNone=0
FIELD System.Byte MagicFirst=82
FIELD System.Byte MagicSecond=86
METHOD System.Byte[] Encode(FinalFactory.Rendezvous.Contracts.PresenceDatagram datagram)
METHOD System.Boolean TryDecode(System.ReadOnlySpan<System.Byte> encoded, FinalFactory.Rendezvous.Contracts.PresenceDatagram& datagram, FinalFactory.Rendezvous.Contracts.UdpDecodeError& error)
TYPE FinalFactory.Rendezvous.Contracts.RenewLeaseRequest
CTOR ()
PROP System.Int32 ContractVersion {get;set;}
PROP System.String LeaseToken {get;set;}
TYPE FinalFactory.Rendezvous.Contracts.RenewLeaseResponse
CTOR ()
PROP System.Int32 ContractVersion {get;set;}
PROP System.DateTimeOffset ExpiresAt {get;set;}
TYPE FinalFactory.Rendezvous.Contracts.ReportConnectionOutcomeRequest
CTOR ()
PROP System.Int32 ContractVersion {get;set;}
PROP System.String DiagnosticCode {get;set;}
PROP System.Int32 ElapsedMilliseconds {get;set;}
PROP FinalFactory.Rendezvous.Contracts.ConnectionOutcomeKind Outcome {get;set;}
TYPE FinalFactory.Rendezvous.Contracts.ReportConnectionOutcomeResponse
CTOR ()
PROP System.Boolean Accepted {get;set;}
PROP System.Int32 ContractVersion {get;set;}
TYPE FinalFactory.Rendezvous.Contracts.SessionCapacity
CTOR ()
PROP System.Int32 CurrentPlayers {get;set;}
PROP System.Int32 MaximumPlayers {get;set;}
TYPE FinalFactory.Rendezvous.Contracts.SessionListing
CTOR ()
PROP System.String BuildVersion {get;set;}
PROP FinalFactory.Rendezvous.Contracts.SessionCapacity Capacity {get;set;}
PROP System.Int32 ContractVersion {get;set;}
PROP System.String DisplayName {get;set;}
PROP FinalFactory.Rendezvous.Contracts.EnvironmentId EnvironmentId {get;set;}
PROP FinalFactory.Rendezvous.Contracts.GameId GameId {get;set;}
PROP FinalFactory.Rendezvous.Contracts.SessionListingId ListingId {get;set;}
PROP System.Collections.Generic.Dictionary<System.String,System.String> Metadata {get;set;}
PROP System.UInt32 ProtocolVersion {get;set;}
PROP FinalFactory.Rendezvous.Contracts.PublisherTrustMode PublisherTrustMode {get;set;}
PROP FinalFactory.Rendezvous.Contracts.RegionId RegionId {get;set;}
PROP FinalFactory.Rendezvous.Contracts.ListingVisibility Visibility {get;set;}
TYPE FinalFactory.Rendezvous.Contracts.SessionListingId
CTOR (System.Guid value)
PROP System.Guid Value {get;}
METHOD System.Boolean Equals(FinalFactory.Rendezvous.Contracts.SessionListingId other)
METHOD System.Boolean Equals(System.Object obj)
METHOD System.Int32 GetHashCode()
METHOD System.String ToString()
METHOD System.Boolean TryParse(System.String value, FinalFactory.Rendezvous.Contracts.SessionListingId& id)
METHOD System.Boolean op_Equality(FinalFactory.Rendezvous.Contracts.SessionListingId left, FinalFactory.Rendezvous.Contracts.SessionListingId right)
METHOD System.Boolean op_Inequality(FinalFactory.Rendezvous.Contracts.SessionListingId left, FinalFactory.Rendezvous.Contracts.SessionListingId right)
TYPE FinalFactory.Rendezvous.Contracts.UdpDecodeError
ENUM None=0
ENUM DatagramTooLarge=1
ENUM Truncated=2
ENUM InvalidMagic=3
ENUM UnsupportedVersion=4
ENUM UnknownMessageType=5
ENUM InvalidFlags=6
ENUM InvalidHandle=7
ENUM InvalidAddressFamily=8
ENUM InvalidAddress=9
ENUM InvalidPort=10
ENUM InvalidCapability=11
ENUM TrailingData=12
TYPE FinalFactory.Rendezvous.Contracts.UdpPresenceMessageType
ENUM HostPresence=1
ENUM ClientPresence=2
TYPE FinalFactory.Rendezvous.Contracts.UpdateSessionRequest
CTOR ()
PROP System.String BuildVersion {get;set;}
PROP FinalFactory.Rendezvous.Contracts.SessionCapacity Capacity {get;set;}
PROP System.Int32 ContractVersion {get;set;}
PROP System.String DisplayName {get;set;}
PROP System.String LeaseToken {get;set;}
PROP System.Collections.Generic.Dictionary<System.String,System.String> Metadata {get;set;}
@@ -0,0 +1 @@
{"contractVersion":1,"attemptId":"11112233-4455-6677-8899-aabbccddeeff","mediationHandle":"22222233-4455-6677-8899-aabbccddeeff","clientPunchCapability":"Abc_123-xYz","expiresAt":"2026-07-16T12:00:00+00:00","dedicatedFallback":{"addressFamily":"ipv6","address":"2001:db8::10","port":9050}}
@@ -0,0 +1 @@
{"contractVersion":1,"idempotencyKey":"register-001","gameId":"space-game","environmentId":"production","regionId":"eu-central","protocolVersion":7,"buildVersion":"1.4.2","displayName":"Europa Relay","visibility":"public","capacity":{"currentPlayers":2,"maximumPlayers":8},"metadata":{"mode":"co-op","map":"europa"}}
@@ -0,0 +1 @@
525601010000112233445566778899aabbccddeeff0404c000020a235a0b4162635f3132332d78597a
@@ -107,7 +107,8 @@
"type": "Project",
"dependencies": {
"FinalFactory.Rendezvous.Contracts": "[1.0.0, )",
"LiteNetLib": "[2.1.4, )"
"LiteNetLib": "[2.1.4, )",
"Microsoft.AspNetCore.OpenApi": "[10.0.9, )"
}
},
"finalfactory.rendezvous.testclient": {
@@ -123,6 +124,21 @@
"requested": "[2.1.4, )",
"resolved": "2.1.4",
"contentHash": "KWlxvMw3Urpqj9joD96LRiK+LC62pQNs/zkXRJc+rHnxgkGp+vV703xzDrxRmv+V1YhCFfIGzs5nrVWtREIlyA=="
},
"Microsoft.AspNetCore.OpenApi": {
"type": "CentralTransitive",
"requested": "[10.0.9, )",
"resolved": "10.0.9",
"contentHash": "1ihb8FO9cGgEK1/m3CTtT/SfnynwmiZib0W2pcDVj3KSWk/Sca4VOXEtaptKQc582zpFrzTFiwkGRCglt6H+WQ==",
"dependencies": {
"Microsoft.OpenApi": "2.0.0"
}
},
"Microsoft.OpenApi": {
"type": "CentralTransitive",
"requested": "[2.7.5, )",
"resolved": "2.7.5",
"contentHash": "0FA67RSnRM4tcBKqiqVu/HPdZ9+QOKbmeRjxRUGTCjPU4C0bmUhd97Dso7Yild5P7nOV6GxJ2xrK0Kv/O9xp0w=="
}
}
}