diff --git a/src/GameCult.Networking/Contracts/cultnet.operation-request.schema.json b/src/GameCult.Networking/Contracts/cultnet.operation-request.schema.json new file mode 100644 index 00000000..07cd9d8d --- /dev/null +++ b/src/GameCult.Networking/Contracts/cultnet.operation-request.schema.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/GameCult/cultnet-ts/contracts/cultnet.operation-request.schema.json", + "title": "CultNet Operation Request", + "type": "object", + "required": ["schemaVersion", "messageId", "serviceId", "operation", "payloadSchema", "payload"], + "additionalProperties": false, + "properties": { + "schemaVersion": { "const": "cultnet.operation_request.v0" }, + "messageId": { "type": "string", "minLength": 1 }, + "serviceId": { "type": "string", "minLength": 1 }, + "operation": { "type": "string", "minLength": 1 }, + "payloadSchema": { "type": "string", "minLength": 1 }, + "payloadEncoding": { "const": "messagepack-base64" }, + "payload": { "type": "string", "minLength": 1, "contentEncoding": "base64" }, + "sourceRuntimeId": { "type": "string", "minLength": 1 }, + "targetRuntimeId": { "type": "string", "minLength": 1 } + } +} diff --git a/src/GameCult.Networking/Contracts/cultnet.operation-response.schema.json b/src/GameCult.Networking/Contracts/cultnet.operation-response.schema.json new file mode 100644 index 00000000..c3be917d --- /dev/null +++ b/src/GameCult.Networking/Contracts/cultnet.operation-response.schema.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/GameCult/cultnet-ts/contracts/cultnet.operation-response.schema.json", + "title": "CultNet Operation Response", + "type": "object", + "required": ["schemaVersion", "messageId", "serviceId", "operation", "status", "payloadSchema", "payload"], + "additionalProperties": false, + "properties": { + "schemaVersion": { "const": "cultnet.operation_response.v0" }, + "messageId": { "type": "string", "minLength": 1 }, + "serviceId": { "type": "string", "minLength": 1 }, + "operation": { "type": "string", "minLength": 1 }, + "status": { "type": "string", "minLength": 1 }, + "payloadSchema": { "type": "string", "minLength": 1 }, + "payloadEncoding": { "const": "messagepack-base64" }, + "payload": { "type": "string", "minLength": 1, "contentEncoding": "base64" }, + "diagnostics": { "type": "array", "items": { "type": "string" } }, + "sourceRuntimeId": { "type": "string", "minLength": 1 } + } +} diff --git a/src/GameCult.Networking/CultNetSchemaMessageSerialization.cs b/src/GameCult.Networking/CultNetSchemaMessageSerialization.cs index 5e9431b4..a8487665 100644 --- a/src/GameCult.Networking/CultNetSchemaMessageSerialization.cs +++ b/src/GameCult.Networking/CultNetSchemaMessageSerialization.cs @@ -55,6 +55,8 @@ namespace GameCult.Networking CultNetSchemaVersions.Error => Deserialize(payload), CultNetSchemaVersions.SampleChangeName => Deserialize(payload), CultNetSchemaVersions.SampleChat => Deserialize(payload), + CultNetSchemaVersions.OperationRequest => Deserialize(payload), + CultNetSchemaVersions.OperationResponse => Deserialize(payload), CultNetSchemaVersions.DocumentDelete => Deserialize(payload), CultNetSchemaVersions.DocumentPutRaw => Deserialize(payload), CultNetSchemaVersions.SnapshotRequest => Deserialize(payload), diff --git a/src/GameCult.Networking/CultNetSchemaMessages.cs b/src/GameCult.Networking/CultNetSchemaMessages.cs index ccac3dbf..a37c2bc0 100644 --- a/src/GameCult.Networking/CultNetSchemaMessages.cs +++ b/src/GameCult.Networking/CultNetSchemaMessages.cs @@ -56,6 +56,14 @@ namespace GameCult.Networking /// public const string SampleChat = "cultnet.sample.chat.v0"; /// + /// Generic typed service operation request contract identifier. + /// + public const string OperationRequest = "cultnet.operation_request.v0"; + /// + /// Generic typed service operation response contract identifier. + /// + public const string OperationResponse = "cultnet.operation_response.v0"; + /// /// document delete contract identifier. /// public const string DocumentDelete = "cultnet.document_delete.v0"; @@ -519,6 +527,60 @@ namespace GameCult.Networking [Key("text")] public string Text { get; set; } = string.Empty; } + /// + /// Invokes one typed operation on an advertised service. + /// + [MessagePackObject] + public class CultNetOperationRequestMessage : ICultNetSchemaMessage + { + /// Gets or sets the wire schema version. + [Key("schemaVersion")] public string SchemaVersion { get; set; } = CultNetSchemaVersions.OperationRequest; + /// Gets or sets the correlation id. + [Key("messageId")] public string MessageId { get; set; } = string.Empty; + /// Gets or sets the advertised service id. + [Key("serviceId")] public string ServiceId { get; set; } = string.Empty; + /// Gets or sets the requested operation. + [Key("operation")] public string Operation { get; set; } = string.Empty; + /// Gets or sets the inner payload schema. + [Key("payloadSchema")] public string PayloadSchema { get; set; } = string.Empty; + /// Gets or sets the inner payload encoding. + [Key("payloadEncoding")] public string PayloadEncoding { get; set; } = "messagepack-base64"; + /// Gets or sets the encoded inner payload. + [Key("payload")] public string Payload { get; set; } = string.Empty; + /// Gets or sets the calling runtime id. + [Key("sourceRuntimeId")] public string? SourceRuntimeId { get; set; } + /// Gets or sets an optional target runtime id. + [Key("targetRuntimeId")] public string? TargetRuntimeId { get; set; } + } + + /// + /// Returns the correlated result of one typed service operation. + /// + [MessagePackObject] + public class CultNetOperationResponseMessage : ICultNetSchemaMessage + { + /// Gets or sets the wire schema version. + [Key("schemaVersion")] public string SchemaVersion { get; set; } = CultNetSchemaVersions.OperationResponse; + /// Gets or sets the request correlation id. + [Key("messageId")] public string MessageId { get; set; } = string.Empty; + /// Gets or sets the advertised service id. + [Key("serviceId")] public string ServiceId { get; set; } = string.Empty; + /// Gets or sets the completed operation. + [Key("operation")] public string Operation { get; set; } = string.Empty; + /// Gets or sets the operation status. + [Key("status")] public string Status { get; set; } = string.Empty; + /// Gets or sets the inner payload schema. + [Key("payloadSchema")] public string PayloadSchema { get; set; } = string.Empty; + /// Gets or sets the inner payload encoding. + [Key("payloadEncoding")] public string PayloadEncoding { get; set; } = "messagepack-base64"; + /// Gets or sets the encoded inner payload. + [Key("payload")] public string Payload { get; set; } = string.Empty; + /// Gets or sets transport-level diagnostics. + [Key("diagnostics")] public string[] Diagnostics { get; set; } = Array.Empty(); + /// Gets or sets the responding runtime id. + [Key("sourceRuntimeId")] public string? SourceRuntimeId { get; set; } + } + /// /// Raw document record carried over CultNet. /// diff --git a/src/GameCult.Networking/CultNetSchemaRegistry.cs b/src/GameCult.Networking/CultNetSchemaRegistry.cs index baa20b5b..037c0d84 100644 --- a/src/GameCult.Networking/CultNetSchemaRegistry.cs +++ b/src/GameCult.Networking/CultNetSchemaRegistry.cs @@ -322,6 +322,20 @@ namespace GameCult.Networking WireContracts = new[] { CultNetWireContracts.SchemaV0, CultNetWireContracts.GameCultNetworkingV0 } }, new SchemaResourceSpec + { + FileName = "cultnet.operation-request.schema.json", + Kind = "wire_message", + SchemaVersion = CultNetSchemaVersions.OperationRequest, + WireContracts = new[] { CultNetWireContracts.SchemaV0 } + }, + new SchemaResourceSpec + { + FileName = "cultnet.operation-response.schema.json", + Kind = "wire_message", + SchemaVersion = CultNetSchemaVersions.OperationResponse, + WireContracts = new[] { CultNetWireContracts.SchemaV0 } + }, + new SchemaResourceSpec { FileName = "cultnet.document-delete.schema.json", Kind = "wire_message", diff --git a/tests/GameCult.Networking.Tests/NetworkingTests.cs b/tests/GameCult.Networking.Tests/NetworkingTests.cs index 526c3fa6..3b54be31 100644 --- a/tests/GameCult.Networking.Tests/NetworkingTests.cs +++ b/tests/GameCult.Networking.Tests/NetworkingTests.cs @@ -431,6 +431,27 @@ namespace GameCult.Networking.Tests Is.EqualTo(new int?[] { 64, 64, 64 })); } + [Test] + public void CultNetSchemaMessageSerialization_RoundTrips_OperationEnvelope() + { + var request = new CultNetOperationRequestMessage + { + MessageId = "plugin-42", + ServiceId = "sai.vn", + Operation = "project", + PayloadSchema = "gamecult.eve.plugin_abi.request.v1", + Payload = "gaZzY2hlbWHEJ2dhbWVjdWx0LmV2ZS5wbHVnaW5fYWJpLnJlcXVlc3QudjE=", + SourceRuntimeId = "eve-unity" + }; + + var payload = CultNetSchemaMessageSerialization.Serialize(request); + var roundTrip = (CultNetOperationRequestMessage)CultNetSchemaMessageSerialization.Deserialize(payload); + + Assert.That(roundTrip.MessageId, Is.EqualTo(request.MessageId)); + Assert.That(roundTrip.ServiceId, Is.EqualTo("sai.vn")); + Assert.That(roundTrip.PayloadSchema, Is.EqualTo("gamecult.eve.plugin_abi.request.v1")); + } + [Test] public void LiteNetLibTransportProfile_AdvertisesLegacyAndSchemaChannels() {