From d38ca32b863d71d7ef323b56fb289dd01d476909 Mon Sep 17 00:00:00 2001 From: Blank Date: Thu, 22 Oct 2026 13:01:55 +0800 Subject: [PATCH] =?UTF-8?q?feat(network-remotemessaging):=20=E7=BB=9F?= =?UTF-8?q?=E4=B8=80=E6=B6=88=E6=81=AF=E5=8F=91=E9=80=81=E5=99=A8=E6=9C=AC?= =?UTF-8?q?=E6=9C=8D=E8=BA=AB=E4=BB=BD=E6=94=B9=E4=B8=BA=E6=9E=84=E9=80=A0?= =?UTF-8?q?=E6=B3=A8=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit UnifiedMessageSender 构造器新增必填 localServerType/localServerId 并由 InitializeWithDefaults 同签名透传,热更桥接装配点传 OnLoadSuccess 的 setting;删除构造期读取 GlobalSettings.CurrentSetting 与 Game 常量回落, 发送端本服判定不再依赖 CurrentSetting 钉住时序;新增本服判定、身份注入 与空白守卫单测,既有两处测试构造同批迁移并保持 player:Game 断言。 BREAKING CHANGE: UnifiedMessageSender 构造器与 UnifiedMessageSenderHolder.InitializeWithDefaults 签名变化,旧签名不再编译,调用方需同批传入本服身份。 Linear: GFX-988 --- CHANGELOG.md | 2 + .../Lifecycle/HotfixModuleLifecycleBridge.cs | 13 +- .../Unified/UnifiedMessageSender.cs | 29 ++- .../Unified/UnifiedMessageSenderHolder.cs | 14 +- .../UnifiedMessaging/UnifiedMessagingTests.cs | 208 ++++++++++++++++++ 5 files changed, 254 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d8b898f6..b5c50080e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -68,6 +68,8 @@ ### Breaking +* 统一消息发送器本服身份改为构造注入(C286,GFX-988):`UnifiedMessageSender` 构造器新增必填 `localServerType`(空白即抛 `ArgumentException`)/ `localServerId`(置于 `instanceSelector` 之后、可选 `metrics` 之前),`UnifiedMessageSenderHolder.InitializeWithDefaults` 签名同步并透传;类内删除构造期读取 `GlobalSettings.CurrentSetting` 与 `GameServerConst.Game` 常量回落——发送端本服判定(`SendToPlayerAsync` Step 3)与指标服务名兜底不再依赖「桥接构造前把 CurrentSetting 钉回首启动 Role」的隐式时序,`--AllInOne` 多 Role 进程下身份取自热更桥接 `OnLoadSuccess` 收到的首个装载 Role 的 `ServerType` / `ServerId`,现网所有流程行为等价。对外破坏性:公开构造器与 `InitializeWithDefaults` 签名变化(RemoteMessaging 为可打包程序集),旧签名不再编译;仓内调用方(热更桥接装配点 + 测试构造)已同批迁移。 + * Online 叶子域命名空间迁移映射(C231 P3 拆包;无 NuGet 包消费者,破坏面纯源码级,仓内消费者 = 单体剩余 9 目录 / Runtime 壳 / Tests,已全量随迁):`GameFrameX.Online.{Assets,Social,Matchmaking,Party} → GameFrameX.Capability.Online.*`(143 域文件,分归 Assets 34 / Social 67 / Matchmaking 29 / Party 13,另 handler 与助手文件随包);`GameFrameX.Online.Runtime.AdminApi.{OnlineAdminApiContract,OnlineAdminApiAdapters} → GameFrameX.Capability.Online.Contracts`;`GameFrameX.Online.Audit.{OnlineAuditQuery,OnlineAuditRecord,OnlineAuditPage,OnlineAuditDomain} → GameFrameX.Capability.Online.Contracts`;`OnlineRuntimeHost` 叶子域 typed 属性(AssetStore/AssetTransactionStore/AssetQuery/GrantService/ChatStore/FriendStore/GroupStore/NotificationStore/ReportStore/SocialGraphStore/MatchListingStore/MatchTicketStore/PartyStore/SocialDecisions/Punishments/Notifications/MatchTickets/Matchmaker/MatchQueueObserver/ChatAudit)退役——产物经能力模块自装配与 `CapabilityHost.Services` 取用(P4 域属性保留:Timeline/Overview/Seasons/Tournaments 在叶子能力被禁用配置下可为 null,其 handler/job 随模块禁用不可达) * P4 上层域命名空间与组合根类型迁移映射(C232 P4;无 NuGet 包消费者,破坏面纯源码级,仓内消费者 = Launcher / Tests,已全量随迁):`GameFrameX.Online.{Match,Leaderboard,Season,Tournament,Audit,HotfixRollback,GameEvents,Timeline,Overview} → GameFrameX.Capability.Online.{Match,Competition.Leaderboard,Competition.Season,Competition.Tournament,Operations.Audit,Operations.HotfixRollback,Operations.GameEvents,Operations.Overview,Operations.Timeline}`;`GameFrameX.Online.Runtime`(LiveOps 域类型)`→ GameFrameX.Capability.Online.Competition.LiveOps`;`GameFrameX.Online.Runtime` 命名空间下的组合根类型(OnlineCapabilityCompositionOptions / OnlineCapabilityComposition / OnlineAdminApiComposition)`→ GameFrameX.Launcher.Online`;`OnlineRuntimeHost` 退役——`OnlineCapabilityComposition.Runtime` 改为机制公开面 `CapabilityHost`(StartAsync/StopAsync 同签名同幂等语义,AppStartUpGame 调用形态零变化) diff --git a/GameFrameX.Hotfix/Lifecycle/HotfixModuleLifecycleBridge.cs b/GameFrameX.Hotfix/Lifecycle/HotfixModuleLifecycleBridge.cs index 83a8d18ee..0099d14b7 100644 --- a/GameFrameX.Hotfix/Lifecycle/HotfixModuleLifecycleBridge.cs +++ b/GameFrameX.Hotfix/Lifecycle/HotfixModuleLifecycleBridge.cs @@ -87,14 +87,17 @@ public async Task OnLoadSuccess(AppSetting setting, bool reload) // 多 Role 进程(--AllInOne)中钉住 CurrentSetting(C285 自 AppStartUpBase.Init 显式化): // Launcher 的创建循环先对全部 Role 逐个 Init,SetCurrentSetting 对 Role 级字段接受最新值, // 循环结束时 CurrentSetting 指向最后创建的 Role;热更首次装载由首个启动的 Role 触发, - // 这里把 CurrentSetting 钉回该首启动 Role,且必须早于 UnifiedMessageSender 构造—— - // 其构造器读取 CurrentSetting 固化本服身份(另有路由/会话等调用期读取方)。 - // 单 Role 形态无差异;直接删除会让上述消费方的服务器身份漂移。 + // 这里把 CurrentSetting 钉回该首启动 Role,供路由/会话等调用期读取方取到稳定身份 + // (发送端身份自 C286 起改为构造注入,不再依赖本钉住的时序与值)。 + // 单 Role 形态无差异;直接删除会让调用期读取方的服务器身份漂移。 + GlobalSettings.SetCurrentSetting(setting); // 初始化统一消息发送器:resolver 按发现层可用性选择—— // 启动流程先 Activate 发现层(含 player_route 读侧),后加载 Hotfix,故此处 // Bootstrap.Resolver 必然已就绪;未激活发现层的形态回落 DefaultPlayerRouteResolver。 // localSender 单实例同时供 UnifiedMessageSenderHolder 与 LocalEnvelopeDispatcher,保证一条本地复投语义。 + // 发送端本服身份(C286)构造注入自本回调收到的 setting,不依赖 CurrentSetting 的钉住时序 + // (钉住仍服务 PlayerSessionManager 等调用期读取方)。 if (!UnifiedMessageSenderHolder.IsInitialized) { var remoteClient = RemoteMessagingBuilder.BuildFromEnvironment(); @@ -105,7 +108,9 @@ public async Task OnLoadSuccess(AppSetting setting, bool reload) UnifiedMessageSenderHolder.InitializeWithDefaults( routeResolver, localSender, - remoteClient); + remoteClient, + setting.ServerType, + setting.ServerId); // holder 初始化后补装路由缝 case 1 的本地投递槽(幂等;时序前提见 AttachLocalDispatcher 注释)。 // 本地投递槽装配统一走组合侧 DiscoveryRoutingWire(两 Provider 共用,不再按实现分支)。 diff --git a/GameFrameX.Network.RemoteMessaging/Unified/UnifiedMessageSender.cs b/GameFrameX.Network.RemoteMessaging/Unified/UnifiedMessageSender.cs index c1301b4cf..7788af33a 100644 --- a/GameFrameX.Network.RemoteMessaging/Unified/UnifiedMessageSender.cs +++ b/GameFrameX.Network.RemoteMessaging/Unified/UnifiedMessageSender.cs @@ -31,7 +31,6 @@ using System.Collections.Concurrent; using GameFrameX.Network.Abstractions; using GameFrameX.Network.RemoteMessaging.Contracts; -using GameFrameX.Utility.Setting; namespace GameFrameX.Network.RemoteMessaging.Unified; @@ -58,34 +57,44 @@ public sealed class UnifiedMessageSender : IUnifiedMessageSender private readonly int _currentServerId; /// - /// 初始化统一消息发送器。 + /// 初始化统一消息发送器。本服身份(本服判定与指标服务名兜底的基准)由调用方在构造时显式注入, + /// 不读取全局配置——多 Role 进程中应传入触发装配的 Role 的 ServerType / ServerId。 /// /// - /// Initializes the unified message sender. + /// Initializes the unified message sender. The local-server identity (the basis for the + /// local-server check and the metrics service-name fallback) is explicitly injected by the caller + /// at construction time instead of reading global settings — in a multi-role process, pass the + /// ServerType / ServerId of the role that triggers the wiring. /// /// 远程消息客户端 / Remote message client /// 玩家路由解析器 / Player route resolver /// 本服玩家发送器 / Local player sender /// 服务实例选择器 / Server instance selector + /// 本服服务器类型(本服判定基准,忽略大小写) / Local server type (basis of the local-server check, case-insensitive) + /// 本服服务器编号(本服判定基准) / Local server id (basis of the local-server check) /// 发送指标聚合器;为空时自动创建 / Metrics recorder; auto-created when null /// 当 为 null 时抛出 / Thrown when is null /// 当 为 null 时抛出 / Thrown when is null /// 当 为 null 时抛出 / Thrown when is null /// 当 为 null 时抛出 / Thrown when is null + /// 当 为 null、空串或空白时抛出 / Thrown when is null, empty, or whitespace public UnifiedMessageSender( IRemoteMessageClient remoteClient, IPlayerRouteResolver routeResolver, IPlayerLocalSender localSender, IServerInstanceSelector instanceSelector, + string localServerType, + int localServerId, MessageSendMetrics metrics = null) { _remoteClient = remoteClient ?? throw new ArgumentNullException(nameof(remoteClient)); _routeResolver = routeResolver ?? throw new ArgumentNullException(nameof(routeResolver)); _localSender = localSender ?? throw new ArgumentNullException(nameof(localSender)); _instanceSelector = instanceSelector ?? throw new ArgumentNullException(nameof(instanceSelector)); + ArgumentException.ThrowIfNullOrWhiteSpace(localServerType, nameof(localServerType)); _metrics = metrics ?? new MessageSendMetrics(); - _currentServerType = GlobalSettings.CurrentSetting?.ServerType ?? GameServerConst.Game.Name; - _currentServerId = GlobalSettings.CurrentSetting?.ServerId ?? GameServerConst.Game.Id; + _currentServerType = localServerType; + _currentServerId = localServerId; } /// @@ -315,6 +324,16 @@ private PlayerSendResult HandleOffline(long playerId, PlayerSendOptions options, } } + /// + /// 判定路由是否指向本服:路由 ServerType(忽略大小写)与 ServerId 同时等于 + /// 构造时注入的本服身份。多 Role 进程中「本服」为单一身份(触发装配的 Role)。 + /// + /// + /// Determines whether the route points to the local server: both the route's + /// ServerType (case-insensitive) and ServerId equal the identity injected at + /// construction. In a multi-role process the "local server" is a single identity + /// (the role that triggered the wiring). + /// private bool IsCurrentServer(PlayerRouteInfo routeInfo) { if (routeInfo == null) diff --git a/GameFrameX.Network.RemoteMessaging/Unified/UnifiedMessageSenderHolder.cs b/GameFrameX.Network.RemoteMessaging/Unified/UnifiedMessageSenderHolder.cs index 2750ab1a2..b8c27e902 100644 --- a/GameFrameX.Network.RemoteMessaging/Unified/UnifiedMessageSenderHolder.cs +++ b/GameFrameX.Network.RemoteMessaging/Unified/UnifiedMessageSenderHolder.cs @@ -121,19 +121,27 @@ public static void Initialize(IUnifiedMessageSender sender) } /// - /// 使用默认组件初始化。适用于标准启动流程。 + /// 使用默认组件初始化。适用于标准启动流程。本服身份由调用方显式传入并透传给发送器, + /// 不读取全局配置。 /// /// /// Initializes with default components. Suitable for standard startup flow. + /// The local-server identity is passed in by the caller and forwarded to the sender + /// instead of being read from global settings. /// /// 玩家路由解析器 / Player route resolver /// 本服玩家发送器 / Local player sender /// 远程消息客户端 / Remote message client + /// 本服服务器类型(透传给发送器的本服判定基准) / Local server type forwarded to the sender as the local-server identity + /// 本服服务器编号(透传给发送器的本服判定基准) / Local server id forwarded to the sender as the local-server identity /// 当 为 null 时抛出 / Thrown when is null + /// 当 为 null、空串或空白时抛出(发送器构造器校验) / Thrown when is null, empty, or whitespace (validated by the sender constructor) public static void InitializeWithDefaults( IPlayerRouteResolver routeResolver, IPlayerLocalSender localSender, - IRemoteMessageClient remoteClient) + IRemoteMessageClient remoteClient, + string localServerType, + int localServerId) { if (remoteClient == null) { @@ -142,7 +150,7 @@ public static void InitializeWithDefaults( var instanceSelector = new ConsistentHashServerInstanceSelector(); var metrics = new MessageSendMetrics(); - var sender = new UnifiedMessageSender(remoteClient, routeResolver, localSender, instanceSelector, metrics); + var sender = new UnifiedMessageSender(remoteClient, routeResolver, localSender, instanceSelector, localServerType, localServerId, metrics); Initialize(sender); } } \ No newline at end of file diff --git a/Tests/GameFrameX.Tests/UnifiedMessaging/UnifiedMessagingTests.cs b/Tests/GameFrameX.Tests/UnifiedMessaging/UnifiedMessagingTests.cs index 68b8972f7..30419238b 100644 --- a/Tests/GameFrameX.Tests/UnifiedMessaging/UnifiedMessagingTests.cs +++ b/Tests/GameFrameX.Tests/UnifiedMessaging/UnifiedMessagingTests.cs @@ -33,6 +33,7 @@ using GameFrameX.Network.RemoteMessaging; using GameFrameX.Network.Messages; using System.Collections; +using GameFrameX.Utility.Setting; using Xunit; namespace GameFrameX.Tests.UnifiedMessaging; @@ -428,6 +429,8 @@ public async Task SendToPlayerAsync_LocalDelivered_RecordsMetrics() new FakePlayerRouteResolver(PlayerRouteInfo.Offline()), new FakePlayerLocalSender(isOnline: true, sendResult: true), new ConsistentHashServerInstanceSelector(), + GameServerConst.Game.Name, + GameServerConst.Game.Id, metrics); var result = await sender.SendToPlayerAsync(1001, new ReqSendToPlayerInner()); @@ -448,6 +451,8 @@ public async Task SendToServerAsync_Success_RecordsMetrics() new FakePlayerRouteResolver(PlayerRouteInfo.Offline()), new FakePlayerLocalSender(isOnline: false, sendResult: false), new ConsistentHashServerInstanceSelector(), + GameServerConst.Game.Name, + GameServerConst.Game.Id, metrics); var result = await sender.SendToServerAsync("social-service", new ReqSendToPlayerInner()); @@ -460,6 +465,127 @@ public async Task SendToServerAsync_Success_RecordsMetrics() } } +public class UnifiedMessageSenderLocalIdentityTests +{ + [Fact] + public async Task SendToPlayerAsync_RouteOnlineDiffersOnlyByCase_TreatedAsLocalServerWithoutRemoteCall() + { + // 注入 ("Social", 77)、路由 ("social", 77):ServerType 仅大小写不同 → 本服;本服投递失败 → 离线,不跨服转发。 + var remoteClient = new CountingRemoteMessageClient(); + var sender = new UnifiedMessageSender( + remoteClient, + new FakePlayerRouteResolver(PlayerRouteInfo.Online("social", 77)), + new FakePlayerLocalSender(isOnline: true, sendResult: false), + new ConsistentHashServerInstanceSelector(), + "Social", + 77); + + var result = await sender.SendToPlayerAsync(2001, new ReqSendToPlayerInner()); + + Assert.False(result.IsSuccess); + Assert.Equal(PlayerDeliverStatus.Offline, result.Status); + Assert.Equal(0, remoteClient.CallWithResultCount); + } + + [Fact] + public async Task SendToPlayerAsync_ExactIdentityMatch_TreatedAsLocalServerWithoutRemoteCall() + { + // 注入 ("Social", 77) 且路由 ("Social", 77):构造期读 CurrentSetting 的旧实现(测试内为 null, + // 回落 Game/1000)会误判为他服并跨服转发;构造注入后判定为本服。 + var remoteClient = new CountingRemoteMessageClient(); + var sender = new UnifiedMessageSender( + remoteClient, + new FakePlayerRouteResolver(PlayerRouteInfo.Online("Social", 77)), + new FakePlayerLocalSender(isOnline: true, sendResult: false), + new ConsistentHashServerInstanceSelector(), + "Social", + 77); + + var result = await sender.SendToPlayerAsync(2002, new ReqSendToPlayerInner()); + + Assert.False(result.IsSuccess); + Assert.Equal(PlayerDeliverStatus.Offline, result.Status); + Assert.Equal(0, remoteClient.CallWithResultCount); + } + + [Fact] + public async Task SendToPlayerAsync_SameServerTypeDifferentServerId_ForwardsToRemoteServer() + { + // 注入 ("Social", 77)、路由 ("Social", 78):ServerId 不同 → 非本服 → 跨服转发。 + var remoteClient = new CountingRemoteMessageClient(); + var sender = new UnifiedMessageSender( + remoteClient, + new FakePlayerRouteResolver(PlayerRouteInfo.Online("Social", 78)), + new FakePlayerLocalSender(isOnline: false, sendResult: false), + new ConsistentHashServerInstanceSelector(), + "Social", + 77); + + var result = await sender.SendToPlayerAsync(2003, new ReqSendToPlayerInner()); + + Assert.True(result.IsSuccess); + Assert.Equal(PlayerDeliverStatus.RemoteDelivered, result.Status); + Assert.Equal(1, remoteClient.CallWithResultCount); + } + + [Fact] + public async Task SendToPlayerAsync_SameServerIdDifferentServerType_ForwardsToRemoteServer() + { + // 注入 ("Social", 77)、路由 ("GateWay", 77):ServerType 不同 → 非本服 → 跨服转发。 + var remoteClient = new CountingRemoteMessageClient(); + var sender = new UnifiedMessageSender( + remoteClient, + new FakePlayerRouteResolver(PlayerRouteInfo.Online("GateWay", 77)), + new FakePlayerLocalSender(isOnline: false, sendResult: false), + new ConsistentHashServerInstanceSelector(), + "Social", + 77); + + var result = await sender.SendToPlayerAsync(2004, new ReqSendToPlayerInner()); + + Assert.True(result.IsSuccess); + Assert.Equal(PlayerDeliverStatus.RemoteDelivered, result.Status); + Assert.Equal(1, remoteClient.CallWithResultCount); + } + + [Fact] + public async Task SendToPlayerAsync_LocalDelivered_MetricsUseInjectedServerType() + { + // 指标服务名兜底取注入身份:注入 "Social" → 本地投递成功指标键 player:Social。 + var metrics = new MessageSendMetrics(); + var sender = new UnifiedMessageSender( + new CountingRemoteMessageClient(), + new FakePlayerRouteResolver(PlayerRouteInfo.Offline()), + new FakePlayerLocalSender(isOnline: true, sendResult: true), + new ConsistentHashServerInstanceSelector(), + "Social", + 77, + metrics); + + var result = await sender.SendToPlayerAsync(2005, new ReqSendToPlayerInner()); + + Assert.True(result.IsSuccess); + Assert.Equal(PlayerDeliverStatus.LocalDelivered, result.Status); + var snapshot = Assert.Single(metrics.GetSnapshots()); + Assert.Equal("player:Social", snapshot.Key); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public void Constructor_NullOrWhiteSpaceLocalServerType_ThrowsArgumentException(string localServerType) + { + Assert.ThrowsAny(() => new UnifiedMessageSender( + new CountingRemoteMessageClient(), + new FakePlayerRouteResolver(PlayerRouteInfo.Offline()), + new FakePlayerLocalSender(isOnline: true, sendResult: true), + new ConsistentHashServerInstanceSelector(), + localServerType, + 77)); + } +} + public class UnifiedMessagingGovernanceTests { [Fact] @@ -626,3 +752,85 @@ private static RemoteCallResult BuildSuccessResult() where return RemoteCallResult.Ok((TResponse)(object)response, elapsedMs: 5, traceId: "trace-test", retryCount: 1); } } + +/// +/// 计数间谍远程客户端:仅 CallWithResultAsync(RemoteCallContext, …) 计数并返回 +/// RespSendToPlayerInner 成功结果(本服判定/跨服转发测试唯一走到的成员),其余成员直接抛 +/// 兜底。 +/// +internal sealed class CountingRemoteMessageClient : IRemoteMessageClient +{ + public int CallWithResultCount { get; private set; } + + public Task CallAsync(string serviceName, MessageObject requestMessage) where TResponse : class, GameFrameX.Network.Abstractions.IResponseMessage + { + throw new NotImplementedException(); + } + + public Task CallAsync(string serviceName, MessageObject requestMessage, CancellationToken cancellationToken) where TResponse : class, GameFrameX.Network.Abstractions.IResponseMessage + { + throw new NotImplementedException(); + } + + public Task CallAsync(string serviceName, MessageObject requestMessage, int timeoutMs, CancellationToken cancellationToken = default) where TResponse : class, GameFrameX.Network.Abstractions.IResponseMessage + { + throw new NotImplementedException(); + } + + public Task> CallWithResultAsync(string serviceName, MessageObject requestMessage) where TResponse : class, GameFrameX.Network.Abstractions.IResponseMessage + { + throw new NotImplementedException(); + } + + public Task> CallWithResultAsync(string serviceName, MessageObject requestMessage, int timeoutMs) where TResponse : class, GameFrameX.Network.Abstractions.IResponseMessage + { + throw new NotImplementedException(); + } + + public Task> CallWithResultAsync(string serviceName, MessageObject requestMessage, int timeoutMs, CancellationToken cancellationToken) where TResponse : class, GameFrameX.Network.Abstractions.IResponseMessage + { + throw new NotImplementedException(); + } + + public Task> CallWithResultAsync(RemoteCallContext context, MessageObject requestMessage) where TResponse : class, GameFrameX.Network.Abstractions.IResponseMessage + { + CallWithResultCount++; + if (typeof(TResponse) != typeof(RespSendToPlayerInner)) + { + throw new NotSupportedException($"Unsupported response type: {typeof(TResponse).Name}"); + } + + var response = new RespSendToPlayerInner + { + Success = true, + PlayerOffline = false, + }; + + return Task.FromResult(RemoteCallResult.Ok((TResponse)(object)response, elapsedMs: 5, traceId: "trace-test", retryCount: 1)); + } + + public Task> CallWithRetryAsync(string serviceName, MessageObject requestMessage, int timeoutMs = RemoteCallContext.DefaultTimeoutMs, int maxRetryCount = RemoteCallContext.DefaultMaxRetryCount, CancellationToken cancellationToken = default) where TResponse : class, GameFrameX.Network.Abstractions.IResponseMessage + { + throw new NotImplementedException(); + } + + public Task> CallWithoutRetryAsync(string serviceName, MessageObject requestMessage, int timeoutMs = RemoteCallContext.DefaultTimeoutMs, CancellationToken cancellationToken = default) where TResponse : class, GameFrameX.Network.Abstractions.IResponseMessage + { + throw new NotImplementedException(); + } + + public Task> CallWithMetadataAsync(string serviceName, MessageObject requestMessage, Dictionary metadata, int timeoutMs = RemoteCallContext.DefaultTimeoutMs, CancellationToken cancellationToken = default) where TResponse : class, GameFrameX.Network.Abstractions.IResponseMessage + { + throw new NotImplementedException(); + } + + public Task SendOneWayAsync(string serviceName, MessageObject requestMessage, int timeoutMs = RemoteCallContext.DefaultTimeoutMs, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public Task IsServiceAvailableAsync(string serviceName) + { + throw new NotImplementedException(); + } +}